summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-11-18 13:16:36 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-11-18 13:16:36 +0000
commit311b0269b4eb9839fa63f80c8d7a58f32b8138a0 (patch)
tree07e7870bca8aed6d61fdcc810731c50d2c40af47 /spec
parent27909cef6c4170ed9205afa7426b8d3de47cbb0c (diff)
downloadgitlab-ce-311b0269b4eb9839fa63f80c8d7a58f32b8138a0.tar.gz
Add latest changes from gitlab-org/gitlab@14-5-stable-eev14.5.0-rc42
Diffstat (limited to 'spec')
-rw-r--r--spec/commands/sidekiq_cluster/cli_spec.rb (renamed from spec/lib/gitlab/sidekiq_cluster/cli_spec.rb)12
-rw-r--r--spec/controllers/admin/integrations_controller_spec.rb2
-rw-r--r--spec/controllers/admin/runners_controller_spec.rb4
-rw-r--r--spec/controllers/application_controller_spec.rb32
-rw-r--r--spec/controllers/concerns/group_tree_spec.rb8
-rw-r--r--spec/controllers/concerns/import_url_params_spec.rb2
-rw-r--r--spec/controllers/concerns/renders_commits_spec.rb6
-rw-r--r--spec/controllers/confirmations_controller_spec.rb41
-rw-r--r--spec/controllers/dashboard/todos_controller_spec.rb2
-rw-r--r--spec/controllers/explore/projects_controller_spec.rb22
-rw-r--r--spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb158
-rw-r--r--spec/controllers/groups/settings/integrations_controller_spec.rb6
-rw-r--r--spec/controllers/groups_controller_spec.rb10
-rw-r--r--spec/controllers/import/bitbucket_controller_spec.rb24
-rw-r--r--spec/controllers/jira_connect/app_descriptor_controller_spec.rb12
-rw-r--r--spec/controllers/jira_connect/events_controller_spec.rb75
-rw-r--r--spec/controllers/oauth/authorizations_controller_spec.rb7
-rw-r--r--spec/controllers/passwords_controller_spec.rb43
-rw-r--r--spec/controllers/profiles/accounts_controller_spec.rb2
-rw-r--r--spec/controllers/profiles/two_factor_auths_controller_spec.rb28
-rw-r--r--spec/controllers/profiles_controller_spec.rb10
-rw-r--r--spec/controllers/projects/alerting/notifications_controller_spec.rb10
-rw-r--r--spec/controllers/projects/analytics/cycle_analytics/stages_controller_spec.rb1
-rw-r--r--spec/controllers/projects/branches_controller_spec.rb6
-rw-r--r--spec/controllers/projects/ci/pipeline_editor_controller_spec.rb20
-rw-r--r--spec/controllers/projects/commits_controller_spec.rb23
-rw-r--r--spec/controllers/projects/hooks_controller_spec.rb2
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb34
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb83
-rw-r--r--spec/controllers/projects/merge_requests/diffs_controller_spec.rb6
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb26
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb29
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb2
-rw-r--r--spec/controllers/projects/prometheus/alerts_controller_spec.rb11
-rw-r--r--spec/controllers/projects/releases_controller_spec.rb13
-rw-r--r--spec/controllers/projects/services_controller_spec.rb6
-rw-r--r--spec/controllers/projects/tags_controller_spec.rb2
-rw-r--r--spec/controllers/projects_controller_spec.rb31
-rw-r--r--spec/controllers/registrations/welcome_controller_spec.rb10
-rw-r--r--spec/controllers/registrations_controller_spec.rb5
-rw-r--r--spec/db/schema_spec.rb9
-rw-r--r--spec/experiments/change_continuous_onboarding_link_urls_experiment_spec.rb53
-rw-r--r--spec/experiments/empty_repo_upload_experiment_spec.rb49
-rw-r--r--spec/factories/analytics/cycle_analytics/issue_stage_events.rb13
-rw-r--r--spec/factories/analytics/cycle_analytics/merge_request_stage_events.rb13
-rw-r--r--spec/factories/authentication_event.rb8
-rw-r--r--spec/factories/ci/builds.rb8
-rw-r--r--spec/factories/ci/job_artifacts.rb11
-rw-r--r--spec/factories/ci/pipelines.rb8
-rw-r--r--spec/factories/ci/reports/security/findings.rb4
-rw-r--r--spec/factories/ci/runner_namespaces.rb9
-rw-r--r--spec/factories/ci/runners.rb9
-rw-r--r--spec/factories/customer_relations/issue_customer_relations_contacts.rb27
-rw-r--r--spec/factories/design_management/designs.rb2
-rw-r--r--spec/factories/error_tracking/error_event.rb4
-rw-r--r--spec/factories/gitlab/database/reindexing/queued_action.rb10
-rw-r--r--spec/factories/group_members.rb13
-rw-r--r--spec/factories/integrations.rb11
-rw-r--r--spec/factories/member_tasks.rb9
-rw-r--r--spec/factories/namespaces/project_namespaces.rb2
-rw-r--r--spec/factories/operations/feature_flags/strategy.rb32
-rw-r--r--spec/factories/packages/helm/file_metadatum.rb6
-rw-r--r--spec/factories/packages/npm/metadata.rb18
-rw-r--r--spec/factories/project_members.rb10
-rw-r--r--spec/factories/user_highest_roles.rb10
-rw-r--r--spec/factories/users/credit_card_validations.rb7
-rw-r--r--spec/factories_spec.rb4
-rw-r--r--spec/features/admin/admin_appearance_spec.rb2
-rw-r--r--spec/features/admin/admin_deploy_keys_spec.rb28
-rw-r--r--spec/features/admin/admin_disables_two_factor_spec.rb1
-rw-r--r--spec/features/admin/admin_groups_spec.rb1
-rw-r--r--spec/features/admin/admin_hooks_spec.rb1
-rw-r--r--spec/features/admin/admin_labels_spec.rb1
-rw-r--r--spec/features/admin/admin_manage_applications_spec.rb1
-rw-r--r--spec/features/admin/admin_runners_spec.rb113
-rw-r--r--spec/features/admin/admin_sees_project_statistics_spec.rb2
-rw-r--r--spec/features/admin/admin_settings_spec.rb8
-rw-r--r--spec/features/admin/admin_users_impersonation_tokens_spec.rb1
-rw-r--r--spec/features/admin/admin_uses_repository_checks_spec.rb1
-rw-r--r--spec/features/admin/clusters/eks_spec.rb2
-rw-r--r--spec/features/admin/users/user_spec.rb1
-rw-r--r--spec/features/admin/users/users_spec.rb7
-rw-r--r--spec/features/alert_management/alert_management_list_spec.rb24
-rw-r--r--spec/features/boards/boards_spec.rb1
-rw-r--r--spec/features/clusters/create_agent_spec.rb44
-rw-r--r--spec/features/contextual_sidebar_spec.rb109
-rw-r--r--spec/features/cycle_analytics_spec.rb24
-rw-r--r--spec/features/dashboard/projects_spec.rb8
-rw-r--r--spec/features/explore/topics_spec.rb25
-rw-r--r--spec/features/graphql_known_operations_spec.rb29
-rw-r--r--spec/features/groups/clusters/eks_spec.rb2
-rw-r--r--spec/features/groups/clusters/user_spec.rb4
-rw-r--r--spec/features/groups/dependency_proxy_spec.rb9
-rw-r--r--spec/features/groups/issues_spec.rb47
-rw-r--r--spec/features/groups/labels/subscription_spec.rb4
-rw-r--r--spec/features/groups/members/leave_group_spec.rb1
-rw-r--r--spec/features/groups/navbar_spec.rb17
-rw-r--r--spec/features/groups/packages_spec.rb4
-rw-r--r--spec/features/groups/settings/manage_applications_spec.rb1
-rw-r--r--spec/features/incidents/user_creates_new_incident_spec.rb55
-rw-r--r--spec/features/incidents/user_views_incident_spec.rb28
-rw-r--r--spec/features/invites_spec.rb14
-rw-r--r--spec/features/issuables/markdown_references/internal_references_spec.rb15
-rw-r--r--spec/features/issue_rebalancing_spec.rb65
-rw-r--r--spec/features/issues/form_spec.rb72
-rw-r--r--spec/features/issues/issue_detail_spec.rb64
-rw-r--r--spec/features/issues/user_creates_issue_spec.rb58
-rw-r--r--spec/features/issues/user_edits_issue_spec.rb5
-rw-r--r--spec/features/issues/user_toggles_subscription_spec.rb2
-rw-r--r--spec/features/issues/user_uses_quick_actions_spec.rb1
-rw-r--r--spec/features/jira_connect/subscriptions_spec.rb4
-rw-r--r--spec/features/merge_request/user_approves_spec.rb2
-rw-r--r--spec/features/merge_request/user_assigns_themselves_spec.rb2
-rw-r--r--spec/features/merge_request/user_comments_on_diff_spec.rb1
-rw-r--r--spec/features/merge_request/user_customizes_merge_commit_message_spec.rb26
-rw-r--r--spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb4
-rw-r--r--spec/features/merge_request/user_posts_diff_notes_spec.rb7
-rw-r--r--spec/features/merge_request/user_posts_notes_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb1
-rw-r--r--spec/features/merge_request/user_sees_deployment_widget_spec.rb1
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_suggest_pipeline_spec.rb40
-rw-r--r--spec/features/oauth_login_spec.rb2
-rw-r--r--spec/features/profile_spec.rb2
-rw-r--r--spec/features/profiles/active_sessions_spec.rb4
-rw-r--r--spec/features/profiles/emails_spec.rb17
-rw-r--r--spec/features/profiles/oauth_applications_spec.rb1
-rw-r--r--spec/features/profiles/personal_access_tokens_spec.rb1
-rw-r--r--spec/features/profiles/two_factor_auths_spec.rb25
-rw-r--r--spec/features/profiles/user_manages_applications_spec.rb1
-rw-r--r--spec/features/profiles/user_manages_emails_spec.rb15
-rw-r--r--spec/features/profiles/user_visits_profile_spec.rb8
-rw-r--r--spec/features/project_variables_spec.rb2
-rw-r--r--spec/features/projects/branches/user_deletes_branch_spec.rb1
-rw-r--r--spec/features/projects/branches_spec.rb1
-rw-r--r--spec/features/projects/cluster_agents_spec.rb53
-rw-r--r--spec/features/projects/clusters/eks_spec.rb3
-rw-r--r--spec/features/projects/clusters/gcp_spec.rb22
-rw-r--r--spec/features/projects/clusters/user_spec.rb6
-rw-r--r--spec/features/projects/clusters_spec.rb26
-rw-r--r--spec/features/projects/commit/comments/user_deletes_comments_spec.rb1
-rw-r--r--spec/features/projects/commit/user_comments_on_commit_spec.rb2
-rw-r--r--spec/features/projects/confluence/user_views_confluence_page_spec.rb3
-rw-r--r--spec/features/projects/environments/environment_spec.rb36
-rw-r--r--spec/features/projects/environments/environments_spec.rb3
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb8
-rw-r--r--spec/features/projects/infrastructure_registry_spec.rb2
-rw-r--r--spec/features/projects/integrations/user_uses_inherited_settings_spec.rb2
-rw-r--r--spec/features/projects/jobs/user_browses_job_spec.rb27
-rw-r--r--spec/features/projects/jobs/user_triggers_manual_job_with_variables_spec.rb34
-rw-r--r--spec/features/projects/members/member_leaves_project_spec.rb1
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb1
-rw-r--r--spec/features/projects/new_project_spec.rb36
-rw-r--r--spec/features/projects/packages_spec.rb4
-rw-r--r--spec/features/projects/pages/user_adds_domain_spec.rb2
-rw-r--r--spec/features/projects/pages/user_edits_lets_encrypt_settings_spec.rb1
-rw-r--r--spec/features/projects/pages/user_edits_settings_spec.rb1
-rw-r--r--spec/features/projects/pipeline_schedules_spec.rb1
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb3
-rw-r--r--spec/features/projects/releases/user_views_releases_spec.rb4
-rw-r--r--spec/features/projects/settings/access_tokens_spec.rb1
-rw-r--r--spec/features/projects/settings/packages_settings_spec.rb4
-rw-r--r--spec/features/projects/settings/service_desk_setting_spec.rb2
-rw-r--r--spec/features/projects/settings/user_searches_in_settings_spec.rb1
-rw-r--r--spec/features/projects/settings/user_tags_project_spec.rb26
-rw-r--r--spec/features/projects/show/no_password_spec.rb11
-rw-r--r--spec/features/projects/show/user_uploads_files_spec.rb28
-rw-r--r--spec/features/projects/user_changes_project_visibility_spec.rb2
-rw-r--r--spec/features/projects/user_creates_project_spec.rb8
-rw-r--r--spec/features/projects_spec.rb12
-rw-r--r--spec/features/signed_commits_spec.rb16
-rw-r--r--spec/features/snippets/notes_on_personal_snippets_spec.rb1
-rw-r--r--spec/features/snippets/user_creates_snippet_spec.rb1
-rw-r--r--spec/features/topic_show_spec.rb48
-rw-r--r--spec/features/triggers_spec.rb1
-rw-r--r--spec/features/users/confirmation_spec.rb30
-rw-r--r--spec/features/users/login_spec.rb6
-rw-r--r--spec/features/users/password_spec.rb30
-rw-r--r--spec/features/users/terms_spec.rb2
-rw-r--r--spec/finders/autocomplete/routes_finder_spec.rb57
-rw-r--r--spec/finders/branches_finder_spec.rb8
-rw-r--r--spec/finders/ci/pipelines_for_merge_request_finder_spec.rb150
-rw-r--r--spec/finders/clusters/agent_authorizations_finder_spec.rb124
-rw-r--r--spec/finders/environments/environments_by_deployments_finder_spec.rb14
-rw-r--r--spec/finders/members_finder_spec.rb8
-rw-r--r--spec/finders/tags_finder_spec.rb81
-rw-r--r--spec/fixtures/api/schemas/analytics/cycle_analytics/summary.json3
-rw-r--r--spec/fixtures/api/schemas/graphql/packages/package_details.json4
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/deploy_key.json25
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/deploy_keys.json4
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/packages/npm_package_version.json12
-rw-r--r--spec/fixtures/bulk_imports/gz/milestones.ndjson.gzbin402 -> 0 bytes
-rw-r--r--spec/fixtures/bulk_imports/milestones.ndjson5
-rw-r--r--spec/fixtures/emails/service_desk_all_quoted.eml22
-rw-r--r--spec/fixtures/emails/service_desk_custom_address_no_key.eml27
-rw-r--r--spec/fixtures/emails/service_desk_forwarded.eml4
-rw-r--r--spec/fixtures/error_tracking/browser_event.json1
-rw-r--r--spec/fixtures/error_tracking/go_parsed_event.json1
-rw-r--r--spec/fixtures/error_tracking/python_event.json1
-rw-r--r--spec/fixtures/gitlab/import_export/lightweight_project_export.tar.gzbin3647 -> 3758 bytes
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/project.json308
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/merge_requests.ndjson16
-rw-r--r--spec/fixtures/packages/npm/payload.json3
-rw-r--r--spec/fixtures/packages/npm/payload_with_duplicated_packages.json3
-rw-r--r--spec/fixtures/scripts/test_report.json36
-rw-r--r--spec/frontend/__helpers__/experimentation_helper.js29
-rw-r--r--spec/frontend/__mocks__/@gitlab/ui.js4
-rw-r--r--spec/frontend/admin/analytics/devops_score/components/devops_score_callout_spec.js4
-rw-r--r--spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js4
-rw-r--r--spec/frontend/admin/deploy_keys/components/table_spec.js47
-rw-r--r--spec/frontend/alert_handler_spec.js12
-rw-r--r--spec/frontend/alert_management/components/alert_management_table_spec.js18
-rw-r--r--spec/frontend/alerts_settings/components/alerts_settings_form_spec.js19
-rw-r--r--spec/frontend/analytics/devops_reports/components/service_ping_disabled_spec.js (renamed from spec/frontend/analytics/devops_report/components/service_ping_disabled_spec.js)4
-rw-r--r--spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js150
-rw-r--r--spec/frontend/batch_comments/components/preview_dropdown_spec.js6
-rw-r--r--spec/frontend/behaviors/gl_emoji_spec.js6
-rw-r--r--spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap14
-rw-r--r--spec/frontend/blob/components/blob_header_spec.js2
-rw-r--r--spec/frontend/blob/components/table_contents_spec.js22
-rw-r--r--spec/frontend/boards/components/board_card_spec.js4
-rw-r--r--spec/frontend/boards/components/board_filtered_search_spec.js33
-rw-r--r--spec/frontend/boards/components/board_form_spec.js18
-rw-r--r--spec/frontend/boards/components/boards_selector_spec.js251
-rw-r--r--spec/frontend/boards/components/issue_board_filtered_search_spec.js57
-rw-r--r--spec/frontend/boards/components/new_board_button_spec.js75
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js1
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js11
-rw-r--r--spec/frontend/boards/mock_data.js87
-rw-r--r--spec/frontend/boards/stores/actions_spec.js72
-rw-r--r--spec/frontend/chronic_duration_spec.js354
-rw-r--r--spec/frontend/clusters/agents/components/show_spec.js63
-rw-r--r--spec/frontend/clusters/components/remove_cluster_confirmation_spec.js45
-rw-r--r--spec/frontend/clusters_list/components/agent_empty_state_spec.js16
-rw-r--r--spec/frontend/clusters_list/components/agent_table_spec.js9
-rw-r--r--spec/frontend/clusters_list/components/agents_spec.js40
-rw-r--r--spec/frontend/clusters_list/components/clusters_actions_spec.js55
-rw-r--r--spec/frontend/clusters_list/components/clusters_empty_state_spec.js104
-rw-r--r--spec/frontend/clusters_list/components/clusters_main_view_spec.js82
-rw-r--r--spec/frontend/clusters_list/components/clusters_spec.js60
-rw-r--r--spec/frontend/clusters_list/components/clusters_view_all_spec.js243
-rw-r--r--spec/frontend/clusters_list/components/install_agent_modal_spec.js38
-rw-r--r--spec/frontend/clusters_list/mocks/apollo.js47
-rw-r--r--spec/frontend/clusters_list/store/mutations_spec.js10
-rw-r--r--spec/frontend/commit/pipelines/pipelines_table_spec.js18
-rw-r--r--spec/frontend/confirm_modal_spec.js4
-rw-r--r--spec/frontend/content_editor/components/content_editor_alert_spec.js (renamed from spec/frontend/content_editor/components/content_editor_error_spec.js)30
-rw-r--r--spec/frontend/content_editor/components/content_editor_spec.js6
-rw-r--r--spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js8
-rw-r--r--spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js8
-rw-r--r--spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js8
-rw-r--r--spec/frontend/content_editor/extensions/attachment_spec.js20
-rw-r--r--spec/frontend/content_editor/extensions/blockquote_spec.js46
-rw-r--r--spec/frontend/content_editor/extensions/emoji_spec.js10
-rw-r--r--spec/frontend/content_editor/extensions/frontmatter_spec.js30
-rw-r--r--spec/frontend/content_editor/extensions/horizontal_rule_spec.js49
-rw-r--r--spec/frontend/content_editor/extensions/inline_diff_spec.js60
-rw-r--r--spec/frontend/content_editor/extensions/link_spec.js91
-rw-r--r--spec/frontend/content_editor/extensions/math_inline_spec.js11
-rw-r--r--spec/frontend/content_editor/extensions/table_of_contents_spec.js32
-rw-r--r--spec/frontend/content_editor/extensions/table_spec.js102
-rw-r--r--spec/frontend/content_editor/extensions/word_break_spec.js35
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js4
-rw-r--r--spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js11
-rw-r--r--spec/frontend/content_editor/test_utils.js23
-rw-r--r--spec/frontend/create_merge_request_dropdown_spec.js5
-rw-r--r--spec/frontend/crm/contacts_root_spec.js60
-rw-r--r--spec/frontend/crm/mock_data.js81
-rw-r--r--spec/frontend/crm/organizations_root_spec.js60
-rw-r--r--spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js10
-rw-r--r--spec/frontend/cycle_analytics/metric_popover_spec.js102
-rw-r--r--spec/frontend/cycle_analytics/mock_data.js41
-rw-r--r--spec/frontend/cycle_analytics/store/actions_spec.js144
-rw-r--r--spec/frontend/cycle_analytics/store/mutations_spec.js1
-rw-r--r--spec/frontend/cycle_analytics/utils_spec.js96
-rw-r--r--spec/frontend/cycle_analytics/value_stream_metrics_spec.js58
-rw-r--r--spec/frontend/delete_label_modal_spec.js4
-rw-r--r--spec/frontend/deploy_keys/components/key_spec.js10
-rw-r--r--spec/frontend/deploy_keys/components/keys_panel_spec.js2
-rw-r--r--spec/frontend/deprecated_jquery_dropdown_spec.js6
-rw-r--r--spec/frontend/design_management/components/list/item_spec.js2
-rw-r--r--spec/frontend/design_management/pages/index_spec.js14
-rw-r--r--spec/frontend/diffs/components/app_spec.js34
-rw-r--r--spec/frontend/diffs/components/diff_discussions_spec.js4
-rw-r--r--spec/frontend/diffs/components/diff_file_header_spec.js23
-rw-r--r--spec/frontend/diffs/components/diff_line_note_form_spec.js89
-rw-r--r--spec/frontend/diffs/components/tree_list_spec.js4
-rw-r--r--spec/frontend/diffs/store/actions_spec.js51
-rw-r--r--spec/frontend/diffs/store/mutations_spec.js24
-rw-r--r--spec/frontend/diffs/utils/diff_line_spec.js30
-rw-r--r--spec/frontend/diffs/utils/discussions_spec.js133
-rw-r--r--spec/frontend/diffs/utils/file_reviews_spec.js24
-rw-r--r--spec/frontend/dropzone_input_spec.js19
-rw-r--r--spec/frontend/editor/helpers.js53
-rw-r--r--spec/frontend/editor/source_editor_extension_base_spec.js68
-rw-r--r--spec/frontend/editor/source_editor_extension_spec.js65
-rw-r--r--spec/frontend/editor/source_editor_instance_spec.js387
-rw-r--r--spec/frontend/editor/source_editor_yaml_ext_spec.js449
-rw-r--r--spec/frontend/environments/graphql/mock_data.js530
-rw-r--r--spec/frontend/environments/graphql/resolvers_spec.js91
-rw-r--r--spec/frontend/environments/new_environment_folder_spec.js74
-rw-r--r--spec/frontend/environments/new_environments_app_spec.js50
-rw-r--r--spec/frontend/experimentation/utils_spec.js198
-rw-r--r--spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js13
-rw-r--r--spec/frontend/filterable_list_spec.js5
-rw-r--r--spec/frontend/fixtures/api_markdown.yml332
-rw-r--r--spec/frontend/fixtures/projects.rb26
-rw-r--r--spec/frontend/flash_spec.js11
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js12
-rw-r--r--spec/frontend/google_cloud/components/app_spec.js66
-rw-r--r--spec/frontend/google_cloud/components/incubation_banner_spec.js60
-rw-r--r--spec/frontend/google_cloud/components/service_accounts_spec.js79
-rw-r--r--spec/frontend/graphql_shared/utils_spec.js4
-rw-r--r--spec/frontend/group_settings/components/shared_runners_form_spec.js47
-rw-r--r--spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap2
-rw-r--r--spec/frontend/ide/components/shared/commit_message_field_spec.js149
-rw-r--r--spec/frontend/ide/stores/mutations_spec.js4
-rw-r--r--spec/frontend/import_entities/components/group_dropdown_spec.js18
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js33
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_source_cell_spec.js27
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_spec.js235
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js96
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js442
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/fixtures.js42
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/services/local_storage_cache_spec.js61
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js64
-rw-r--r--spec/frontend/import_entities/import_groups/services/status_poller_spec.js (renamed from spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js)9
-rw-r--r--spec/frontend/incidents/components/incidents_list_spec.js26
-rw-r--r--spec/frontend/integrations/edit/components/dynamic_field_spec.js229
-rw-r--r--spec/frontend/integrations/edit/components/jira_issues_fields_spec.js87
-rw-r--r--spec/frontend/integrations/integration_settings_form_spec.js303
-rw-r--r--spec/frontend/invite_members/components/confetti_spec.js28
-rw-r--r--spec/frontend/invite_members/components/invite_members_modal_spec.js210
-rw-r--r--spec/frontend/invite_members/components/invite_members_trigger_spec.js35
-rw-r--r--spec/frontend/issuable/components/csv_import_modal_spec.js8
-rw-r--r--spec/frontend/issue_show/components/app_spec.js38
-rw-r--r--spec/frontend/issue_show/components/description_spec.js22
-rw-r--r--spec/frontend/issue_show/components/fields/type_spec.js26
-rw-r--r--spec/frontend/issues_list/components/issues_list_app_spec.js2
-rw-r--r--spec/frontend/issues_list/components/new_issue_dropdown_spec.js6
-rw-r--r--spec/frontend/issues_list/mock_data.js86
-rw-r--r--spec/frontend/issues_list/utils_spec.js8
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/add_namespace_button_spec.js44
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/add_namespace_modal_spec.js36
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js (renamed from spec/frontend/jira_connect/subscriptions/components/groups_list_item_spec.js)4
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js (renamed from spec/frontend/jira_connect/subscriptions/components/groups_list_spec.js)6
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/app_spec.js176
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/sign_in_button_spec.js48
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js50
-rw-r--r--spec/frontend/jira_connect/subscriptions/index_spec.js36
-rw-r--r--spec/frontend/jira_connect/subscriptions/utils_spec.js22
-rw-r--r--spec/frontend/jobs/components/manual_variables_form_spec.js152
-rw-r--r--spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js150
-rw-r--r--spec/frontend/lib/utils/common_utils_spec.js23
-rw-r--r--spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js59
-rw-r--r--spec/frontend/lib/utils/datetime_utility_spec.js10
-rw-r--r--spec/frontend/lib/utils/file_upload_spec.js28
-rw-r--r--spec/frontend/lib/utils/text_markdown_spec.js4
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js8
-rw-r--r--spec/frontend/members/mock_data.js2
-rw-r--r--spec/frontend/monitoring/__snapshots__/alert_widget_spec.js.snap43
-rw-r--r--spec/frontend/monitoring/alert_widget_spec.js423
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap2
-rw-r--r--spec/frontend/monitoring/components/alert_widget_form_spec.js242
-rw-r--r--spec/frontend/monitoring/components/charts/anomaly_spec.js4
-rw-r--r--spec/frontend/monitoring/components/charts/time_series_spec.js1
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_builder_spec.js1
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_spec.js106
-rw-r--r--spec/frontend/monitoring/components/dashboard_spec.js33
-rw-r--r--spec/frontend/monitoring/components/dashboard_url_time_spec.js1
-rw-r--r--spec/frontend/monitoring/components/links_section_spec.js10
-rw-r--r--spec/frontend/monitoring/components/variables/text_field_spec.js4
-rw-r--r--spec/frontend/monitoring/pages/dashboard_page_spec.js4
-rw-r--r--spec/frontend/monitoring/router_spec.js3
-rw-r--r--spec/frontend/notes/components/discussion_counter_spec.js6
-rw-r--r--spec/frontend/notes/components/discussion_notes_spec.js4
-rw-r--r--spec/frontend/notes/components/multiline_comment_form_spec.js12
-rw-r--r--spec/frontend/notes/components/note_body_spec.js1
-rw-r--r--spec/frontend/notes/components/note_form_spec.js2
-rw-r--r--spec/frontend/notes/components/noteable_discussion_spec.js10
-rw-r--r--spec/frontend/notes/components/notes_app_spec.js59
-rw-r--r--spec/frontend/notes/mixins/discussion_navigation_spec.js61
-rw-r--r--spec/frontend/notes/stores/actions_spec.js91
-rw-r--r--spec/frontend/notes/stores/mutation_spec.js10
-rw-r--r--spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap6
-rw-r--r--spec/frontend/packages/list/components/packages_list_app_spec.js45
-rw-r--r--spec/frontend/packages/list/components/packages_search_spec.js128
-rw-r--r--spec/frontend/packages/list/components/packages_title_spec.js71
-rw-r--r--spec/frontend/packages/list/components/tokens/package_type_token_spec.js48
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/__snapshots__/registry_breadcrumb_spec.js.snap (renamed from spec/frontend/registry/explorer/components/__snapshots__/registry_breadcrumb_spec.js.snap)62
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/delete_button_spec.js (renamed from spec/frontend/registry/explorer/components/delete_button_spec.js)2
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/delete_image_spec.js (renamed from spec/frontend/registry/explorer/components/delete_image_spec.js)8
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/__snapshots__/tags_loader_spec.js.snap (renamed from spec/frontend/registry/explorer/components/details_page/__snapshots__/tags_loader_spec.js.snap)0
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js (renamed from spec/frontend/registry/explorer/components/details_page/delete_alert_spec.js)4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_modal_spec.js (renamed from spec/frontend/registry/explorer/components/details_page/delete_modal_spec.js)4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js (renamed from spec/frontend/registry/explorer/components/details_page/details_header_spec.js)8
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/empty_state_spec.js (renamed from spec/frontend/registry/explorer/components/details_page/empty_state_spec.js)4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert_spec.js (renamed from spec/frontend/registry/explorer/components/details_page/partial_cleanup_alert_spec.js)7
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/status_alert_spec.js (renamed from spec/frontend/registry/explorer/components/details_page/status_alert_spec.js)4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js (renamed from spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js)4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js (renamed from spec/frontend/registry/explorer/components/details_page/tags_list_spec.js)15
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_loader_spec.js (renamed from spec/frontend/registry/explorer/components/details_page/tags_loader_spec.js)2
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/__snapshots__/group_empty_state_spec.js.snap (renamed from spec/frontend/registry/explorer/components/list_page/__snapshots__/group_empty_state_spec.js.snap)0
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap (renamed from spec/frontend/registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap)0
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js (renamed from spec/frontend/registry/explorer/components/list_page/cleanup_status_spec.js)4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cli_commands_spec.js (renamed from spec/frontend/registry/explorer/components/list_page/cli_commands_spec.js)4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state_spec.js (renamed from spec/frontend/registry/explorer/components/list_page/group_empty_state_spec.js)2
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js (renamed from spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js)8
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_spec.js (renamed from spec/frontend/registry/explorer/components/list_page/image_list_spec.js)4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state_spec.js (renamed from spec/frontend/registry/explorer/components/list_page/project_empty_state_spec.js)2
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js (renamed from spec/frontend/registry/explorer/components/list_page/registry_header_spec.js)4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/registry_breadcrumb_spec.js (renamed from spec/frontend/registry/explorer/components/registry_breadcrumb_spec.js)2
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js (renamed from spec/frontend/registry/explorer/mock_data.js)0
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js (renamed from spec/frontend/registry/explorer/pages/details_spec.js)24
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/pages/index_spec.js (renamed from spec/frontend/registry/explorer/pages/index_spec.js)2
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js (renamed from spec/frontend/registry/explorer/pages/list_spec.js)22
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/stubs.js (renamed from spec/frontend/registry/explorer/stubs.js)2
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/app_spec.js99
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js84
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/components/manifest_row_spec.js59
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/mock_data.js21
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap16
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/version_row_spec.js.snap1
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js53
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/installations_commands_spec.js14
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/functional/delete_package_spec.js160
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/app_spec.js.snap57
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/app_spec.js168
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js244
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js8
-rw-r--r--spec/frontend/packages_and_registries/package_registry/mock_data.js17
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/expiration_input_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/expiration_run_text_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/expiration_toggle_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/shared/mocks.js (renamed from spec/frontend/registry/shared/mocks.js)0
-rw-r--r--spec/frontend/packages_and_registries/shared/stubs.js (renamed from spec/frontend/registry/shared/stubs.js)0
-rw-r--r--spec/frontend/pages/admin/projects/components/namespace_select_spec.js8
-rw-r--r--spec/frontend/pages/dashboard/todos/index/todos_spec.js4
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap7
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js33
-rw-r--r--spec/frontend/pages/shared/wikis/components/wiki_form_spec.js26
-rw-r--r--spec/frontend/pipeline_editor/components/commit/commit_form_spec.js19
-rw-r--r--spec/frontend/pipeline_editor/components/commit/commit_section_spec.js16
-rw-r--r--spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js37
-rw-r--r--spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js38
-rw-r--r--spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js39
-rw-r--r--spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js72
-rw-r--r--spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js132
-rw-r--r--spec/frontend/pipeline_editor/components/ui/pipeline_editor_messages_spec.js2
-rw-r--r--spec/frontend/pipeline_editor/components/walkthrough_popover_spec.js29
-rw-r--r--spec/frontend/pipeline_editor/mock_data.js82
-rw-r--r--spec/frontend/pipeline_editor/pipeline_editor_app_spec.js117
-rw-r--r--spec/frontend/pipeline_editor/pipeline_editor_home_spec.js106
-rw-r--r--spec/frontend/pipelines/empty_state_spec.js2
-rw-r--r--spec/frontend/pipelines/graph/graph_component_wrapper_spec.js2
-rw-r--r--spec/frontend/pipelines/graph/graph_view_selector_spec.js2
-rw-r--r--spec/frontend/pipelines/pipelines_artifacts_spec.js83
-rw-r--r--spec/frontend/pipelines/pipelines_spec.js6
-rw-r--r--spec/frontend/pipelines/pipelines_table_spec.js6
-rw-r--r--spec/frontend/projects/commit/components/form_modal_spec.js15
-rw-r--r--spec/frontend/projects/commits/components/author_select_spec.js2
-rw-r--r--spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap41
-rw-r--r--spec/frontend/projects/components/project_delete_button_spec.js12
-rw-r--r--spec/frontend/projects/details/upload_button_spec.js7
-rw-r--r--spec/frontend/projects/new/components/new_project_url_select_spec.js39
-rw-r--r--spec/frontend/projects/pipelines/charts/components/app_spec.js24
-rw-r--r--spec/frontend/projects/projects_filterable_list_spec.js5
-rw-r--r--spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js98
-rw-r--r--spec/frontend/projects/settings_service_desk/components/mock_data.js8
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js2
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js80
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_template_dropdown_spec.js80
-rw-r--r--spec/frontend/projects/storage_counter/components/storage_table_spec.js5
-rw-r--r--spec/frontend/projects/storage_counter/components/storage_type_icon_spec.js41
-rw-r--r--spec/frontend/projects/storage_counter/mock_data.js33
-rw-r--r--spec/frontend/projects/storage_counter/utils_spec.js17
-rw-r--r--spec/frontend/projects/upload_file_experiment_tracking_spec.js43
-rw-r--r--spec/frontend/related_merge_requests/components/related_merge_requests_spec.js4
-rw-r--r--spec/frontend/releases/components/tag_field_new_spec.js4
-rw-r--r--spec/frontend/repository/components/blob_content_viewer_spec.js399
-rw-r--r--spec/frontend/repository/components/upload_blob_modal_spec.js10
-rw-r--r--spec/frontend/repository/mock_data.js57
-rw-r--r--spec/frontend/runner/admin_runners/admin_runners_app_spec.js39
-rw-r--r--spec/frontend/runner/components/cells/runner_actions_cell_spec.js27
-rw-r--r--spec/frontend/runner/components/cells/runner_status_cell_spec.js69
-rw-r--r--spec/frontend/runner/components/cells/runner_summary_cell_spec.js39
-rw-r--r--spec/frontend/runner/components/cells/runner_type_cell_spec.js48
-rw-r--r--spec/frontend/runner/components/helpers/masked_value_spec.js51
-rw-r--r--spec/frontend/runner/components/registration/registration_dropdown_spec.js169
-rw-r--r--spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js (renamed from spec/frontend/runner/components/runner_registration_token_reset_spec.js)45
-rw-r--r--spec/frontend/runner/components/registration/registration_token_spec.js109
-rw-r--r--spec/frontend/runner/components/runner_contacted_state_badge_spec.js86
-rw-r--r--spec/frontend/runner/components/runner_filtered_search_bar_spec.js60
-rw-r--r--spec/frontend/runner/components/runner_list_spec.js59
-rw-r--r--spec/frontend/runner/components/runner_manual_setup_help_spec.js122
-rw-r--r--spec/frontend/runner/components/runner_paused_badge_spec.js (renamed from spec/frontend/runner/components/runner_state_paused_badge_spec.js)2
-rw-r--r--spec/frontend/runner/components/runner_state_locked_badge_spec.js45
-rw-r--r--spec/frontend/runner/components/runner_tag_spec.js46
-rw-r--r--spec/frontend/runner/components/runner_tags_spec.js10
-rw-r--r--spec/frontend/runner/components/runner_type_alert_spec.js14
-rw-r--r--spec/frontend/runner/components/runner_type_badge_spec.js14
-rw-r--r--spec/frontend/runner/components/runner_type_tabs_spec.js109
-rw-r--r--spec/frontend/runner/group_runners/group_runners_app_spec.js37
-rw-r--r--spec/frontend/runner/runner_search_utils_spec.js36
-rw-r--r--spec/frontend/search/sidebar/components/app_spec.js56
-rw-r--r--spec/frontend/search/store/actions_spec.js31
-rw-r--r--spec/frontend/search/store/mutations_spec.js10
-rw-r--r--spec/frontend/search/store/utils_spec.js29
-rw-r--r--spec/frontend/security_configuration/components/app_spec.js43
-rw-r--r--spec/frontend/security_configuration/components/feature_card_spec.js11
-rw-r--r--spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js21
-rw-r--r--spec/frontend/sidebar/components/attention_required_toggle_spec.js84
-rw-r--r--spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js20
-rw-r--r--spec/frontend/sidebar/components/time_tracking/report_spec.js2
-rw-r--r--spec/frontend/sidebar/sidebar_mediator_spec.js55
-rw-r--r--spec/frontend/task_list_spec.js17
-rw-r--r--spec/frontend/terms/components/app_spec.js171
-rw-r--r--spec/frontend/test_setup.js6
-rw-r--r--spec/frontend/vue_mr_widget/components/approvals/approvals_summary_spec.js53
-rw-r--r--spec/frontend/vue_mr_widget/components/extensions/actions_spec.js12
-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_suggest_pipeline_spec.js25
-rw-r--r--spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap4
-rw-r--r--spec/frontend/vue_mr_widget/components/states/__snapshots__/new_ready_to_merge_spec.js.snap4
-rw-r--r--spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js11
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js6
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_merging_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js13
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js8
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js8
-rw-r--r--spec/frontend/vue_mr_widget/mock_data.js2
-rw-r--r--spec/frontend/vue_mr_widget/mr_widget_options_spec.js28
-rw-r--r--spec/frontend/vue_mr_widget/stores/get_state_key_spec.js9
-rw-r--r--spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/test_extension.js2
-rw-r--r--spec/frontend/vue_shared/components/alerts_deprecation_warning_spec.js48
-rw-r--r--spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js99
-rw-r--r--spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js61
-rw-r--r--spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js27
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js26
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js29
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js44
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js78
-rw-r--r--spec/frontend/vue_shared/components/header_ci_component_spec.js48
-rw-r--r--spec/frontend/vue_shared/components/notes/system_note_spec.js50
-rw-r--r--spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/project_selector/project_selector_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/registry/title_area_spec.js59
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js67
-rw-r--r--spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap1
-rw-r--r--spec/frontend/vue_shared/components/settings/settings_block_spec.js48
-rw-r--r--spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js76
-rw-r--r--spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js119
-rw-r--r--spec/frontend/vue_shared/components/sidebar/date_picker_spec.js69
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js121
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js72
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js18
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js19
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js69
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_footer_spec.js57
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_header_spec.js75
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js7
-rw-r--r--spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js52
-rw-r--r--spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js2
-rw-r--r--spec/frontend/whats_new/utils/notification_spec.js6
-rw-r--r--spec/frontend/work_items/components/app_spec.js24
-rw-r--r--spec/frontend/work_items/mock_data.js17
-rw-r--r--spec/frontend/work_items/pages/work_item_root_spec.js70
-rw-r--r--spec/frontend/work_items/router_spec.js30
-rw-r--r--spec/graphql/mutations/customer_relations/contacts/create_spec.rb6
-rw-r--r--spec/graphql/mutations/customer_relations/contacts/update_spec.rb6
-rw-r--r--spec/graphql/mutations/customer_relations/organizations/create_spec.rb6
-rw-r--r--spec/graphql/mutations/customer_relations/organizations/update_spec.rb6
-rw-r--r--spec/graphql/mutations/discussions/toggle_resolve_spec.rb4
-rw-r--r--spec/graphql/mutations/environments/canary_ingress/update_spec.rb2
-rw-r--r--spec/graphql/mutations/merge_requests/set_wip_spec.rb55
-rw-r--r--spec/graphql/mutations/notes/reposition_image_diff_note_spec.rb2
-rw-r--r--spec/graphql/mutations/releases/delete_spec.rb2
-rw-r--r--spec/graphql/mutations/releases/update_spec.rb4
-rw-r--r--spec/graphql/mutations/security/ci_configuration/configure_sast_iac_spec.rb13
-rw-r--r--spec/graphql/resolvers/concerns/resolves_groups_spec.rb71
-rw-r--r--spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb22
-rw-r--r--spec/graphql/resolvers/group_issues_resolver_spec.rb65
-rw-r--r--spec/graphql/resolvers/issues_resolver_spec.rb120
-rw-r--r--spec/graphql/resolvers/merge_requests_resolver_spec.rb48
-rw-r--r--spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb5
-rw-r--r--spec/graphql/resolvers/timelog_resolver_spec.rb50
-rw-r--r--spec/graphql/resolvers/topics_resolver_spec.rb33
-rw-r--r--spec/graphql/types/alert_management/prometheus_integration_type_spec.rb2
-rw-r--r--spec/graphql/types/ci/job_artifact_type_spec.rb2
-rw-r--r--spec/graphql/types/ci/pipeline_scope_enum_spec.rb11
-rw-r--r--spec/graphql/types/ci/pipeline_status_enum_spec.rb11
-rw-r--r--spec/graphql/types/ci/pipeline_type_spec.rb4
-rw-r--r--spec/graphql/types/commit_type_spec.rb2
-rw-r--r--spec/graphql/types/customer_relations/contact_type_spec.rb2
-rw-r--r--spec/graphql/types/customer_relations/organization_type_spec.rb2
-rw-r--r--spec/graphql/types/dependency_proxy/manifest_type_spec.rb2
-rw-r--r--spec/graphql/types/evidence_type_spec.rb2
-rw-r--r--spec/graphql/types/merge_request_review_state_enum_spec.rb4
-rw-r--r--spec/graphql/types/merge_request_type_spec.rb2
-rw-r--r--spec/graphql/types/mutation_type_spec.rb8
-rw-r--r--spec/graphql/types/packages/helm/dependency_type_spec.rb15
-rw-r--r--spec/graphql/types/packages/helm/file_metadatum_type_spec.rb15
-rw-r--r--spec/graphql/types/packages/helm/maintainer_type_spec.rb15
-rw-r--r--spec/graphql/types/packages/helm/metadata_type_spec.rb15
-rw-r--r--spec/graphql/types/project_type_spec.rb4
-rw-r--r--spec/graphql/types/projects/topic_type_spec.rb17
-rw-r--r--spec/graphql/types/query_type_spec.rb1
-rw-r--r--spec/graphql/types/release_links_type_spec.rb44
-rw-r--r--spec/graphql/types/repository/blob_type_spec.rb1
-rw-r--r--spec/graphql/types/user_merge_request_interaction_type_spec.rb5
-rw-r--r--spec/helpers/admin/deploy_key_helper_spec.rb28
-rw-r--r--spec/helpers/boards_helper_spec.rb6
-rw-r--r--spec/helpers/ci/pipelines_helper_spec.rb22
-rw-r--r--spec/helpers/ci/runners_helper_spec.rb72
-rw-r--r--spec/helpers/clusters_helper_spec.rb120
-rw-r--r--spec/helpers/emoji_helper_spec.rb5
-rw-r--r--spec/helpers/environments_helper_spec.rb64
-rw-r--r--spec/helpers/graph_helper_spec.rb12
-rw-r--r--spec/helpers/groups/settings_helper_spec.rb38
-rw-r--r--spec/helpers/groups_helper_spec.rb2
-rw-r--r--spec/helpers/invite_members_helper_spec.rb79
-rw-r--r--spec/helpers/issuables_description_templates_helper_spec.rb14
-rw-r--r--spec/helpers/issuables_helper_spec.rb23
-rw-r--r--spec/helpers/issues_helper_spec.rb1
-rw-r--r--spec/helpers/learn_gitlab_helper_spec.rb155
-rw-r--r--spec/helpers/members_helper_spec.rb6
-rw-r--r--spec/helpers/nav/top_nav_helper_spec.rb5
-rw-r--r--spec/helpers/notes_helper_spec.rb12
-rw-r--r--spec/helpers/one_trust_helper_spec.rb19
-rw-r--r--spec/helpers/projects/alert_management_helper_spec.rb33
-rw-r--r--spec/helpers/projects/incidents_helper_spec.rb49
-rw-r--r--spec/helpers/projects/security/configuration_helper_spec.rb2
-rw-r--r--spec/helpers/projects_helper_spec.rb18
-rw-r--r--spec/helpers/routing/pseudonymization_helper_spec.rb220
-rw-r--r--spec/helpers/storage_helper_spec.rb19
-rw-r--r--spec/helpers/tab_helper_spec.rb28
-rw-r--r--spec/helpers/terms_helper_spec.rb44
-rw-r--r--spec/helpers/time_zone_helper_spec.rb32
-rw-r--r--spec/helpers/user_callouts_helper_spec.rb14
-rw-r--r--spec/helpers/users_helper_spec.rb2
-rw-r--r--spec/helpers/wiki_helper_spec.rb6
-rw-r--r--spec/initializers/0_postgresql_types_spec.rb16
-rw-r--r--spec/initializers/100_patch_omniauth_oauth2_spec.rb45
-rw-r--r--spec/initializers/carrierwave_patch_spec.rb3
-rw-r--r--spec/initializers/database_config_spec.rb49
-rw-r--r--spec/initializers/session_store_spec.rb37
-rw-r--r--spec/lib/api/ci/helpers/runner_spec.rb8
-rw-r--r--spec/lib/api/entities/projects/topic_spec.rb19
-rw-r--r--spec/lib/api/helpers_spec.rb2
-rw-r--r--spec/lib/atlassian/jira_connect/client_spec.rb10
-rw-r--r--spec/lib/banzai/filter/emoji_filter_spec.rb6
-rw-r--r--spec/lib/banzai/filter/footnote_filter_spec.rb88
-rw-r--r--spec/lib/banzai/filter/markdown_filter_spec.rb153
-rw-r--r--spec/lib/banzai/filter/plantuml_filter_spec.rb73
-rw-r--r--spec/lib/banzai/filter/sanitization_filter_spec.rb64
-rw-r--r--spec/lib/banzai/filter/syntax_highlight_filter_spec.rb232
-rw-r--r--spec/lib/banzai/pipeline/emoji_pipeline_spec.rb6
-rw-r--r--spec/lib/banzai/pipeline/full_pipeline_spec.rb67
-rw-r--r--spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb55
-rw-r--r--spec/lib/banzai/renderer_spec.rb18
-rw-r--r--spec/lib/bulk_imports/common/pipelines/milestones_pipeline_spec.rb154
-rw-r--r--spec/lib/bulk_imports/common/pipelines/uploads_pipeline_spec.rb80
-rw-r--r--spec/lib/bulk_imports/common/pipelines/wiki_pipeline_spec.rb25
-rw-r--r--spec/lib/bulk_imports/groups/graphql/get_milestones_query_spec.rb35
-rw-r--r--spec/lib/bulk_imports/groups/loaders/group_loader_spec.rb58
-rw-r--r--spec/lib/bulk_imports/groups/pipelines/milestones_pipeline_spec.rb73
-rw-r--r--spec/lib/bulk_imports/groups/stage_spec.rb2
-rw-r--r--spec/lib/bulk_imports/ndjson_pipeline_spec.rb3
-rw-r--r--spec/lib/bulk_imports/projects/pipelines/external_pull_requests_pipeline_spec.rb66
-rw-r--r--spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb297
-rw-r--r--spec/lib/bulk_imports/projects/pipelines/protected_branches_pipeline_spec.rb61
-rw-r--r--spec/lib/bulk_imports/projects/pipelines/repository_pipeline_spec.rb97
-rw-r--r--spec/lib/bulk_imports/projects/stage_spec.rb11
-rw-r--r--spec/lib/container_registry/client_spec.rb2
-rw-r--r--spec/lib/container_registry/tag_spec.rb2
-rw-r--r--spec/lib/error_tracking/collector/payload_validator_spec.rb49
-rw-r--r--spec/lib/error_tracking/collector/sentry_request_parser_spec.rb7
-rw-r--r--spec/lib/feature/gitaly_spec.rb4
-rw-r--r--spec/lib/feature_spec.rb16
-rw-r--r--spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb11
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/aggregated/base_query_builder_spec.rb150
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher_spec.rb130
-rw-r--r--spec/lib/gitlab/application_rate_limiter_spec.rb132
-rw-r--r--spec/lib/gitlab/asciidoc_spec.rb1351
-rw-r--r--spec/lib/gitlab/auth/auth_finders_spec.rb64
-rw-r--r--spec/lib/gitlab/background_migration/add_modified_to_approval_merge_request_rule_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/add_primary_email_to_emails_if_user_confirmed_spec.rb49
-rw-r--r--spec/lib/gitlab/background_migration/backfill_artifact_expiry_date_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/backfill_deployment_clusters_from_deployments_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/backfill_design_internal_ids_spec.rb69
-rw-r--r--spec/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/backfill_jira_tracker_deployment_type2_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/backfill_merge_request_cleanup_schedules_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/backfill_namespace_settings_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/backfill_project_settings_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/backfill_push_rules_id_in_projects_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/backfill_user_namespace_spec.rb39
-rw-r--r--spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/copy_merge_request_target_project_to_merge_request_metrics_spec.rb39
-rw-r--r--spec/lib/gitlab/background_migration/drop_invalid_vulnerabilities_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/fix_merge_request_diff_commit_users_spec.rb316
-rw-r--r--spec/lib/gitlab/background_migration/fix_projects_without_project_feature_spec.rb75
-rw-r--r--spec/lib/gitlab/background_migration/fix_projects_without_prometheus_service_spec.rb234
-rw-r--r--spec/lib/gitlab/background_migration/job_coordinator_spec.rb344
-rw-r--r--spec/lib/gitlab/background_migration/link_lfs_objects_projects_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/migrate_fingerprint_sha256_within_keys_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data_spec.rb327
-rw-r--r--spec/lib/gitlab/background_migration/migrate_merge_request_diff_commit_users_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/migrate_u2f_webauthn_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/migrate_users_bio_to_user_details_spec.rb85
-rw-r--r--spec/lib/gitlab/background_migration/populate_canonical_emails_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/populate_dismissed_state_for_vulnerabilities_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/populate_finding_uuid_for_vulnerability_feedback_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/populate_has_vulnerabilities_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/populate_issue_email_participants_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/populate_missing_vulnerability_dismissal_information_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/populate_personal_snippet_statistics_spec.rb4
-rw-r--r--spec/lib/gitlab/background_migration/populate_project_snippet_statistics_spec.rb4
-rw-r--r--spec/lib/gitlab/background_migration/populate_user_highest_roles_table_spec.rb71
-rw-r--r--spec/lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces_spec.rb254
-rw-r--r--spec/lib/gitlab/background_migration/recalculate_project_authorizations_with_min_max_user_id_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/remove_duplicate_services_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb77
-rw-r--r--spec/lib/gitlab/background_migration/replace_blocked_by_links_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/reset_shared_runners_for_transferred_projects_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/set_default_iteration_cadences_spec.rb80
-rw-r--r--spec/lib/gitlab/background_migration/set_merge_request_diff_files_count_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/set_null_external_diff_store_to_local_value_spec.rb33
-rw-r--r--spec/lib/gitlab/background_migration/set_null_package_files_file_store_to_local_value_spec.rb33
-rw-r--r--spec/lib/gitlab/background_migration/steal_migrate_merge_request_diff_commit_users_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/update_existing_subgroup_to_match_visibility_level_of_parent_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/update_existing_users_that_require_two_factor_auth_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/user_mentions/create_resource_user_mention_spec.rb114
-rw-r--r--spec/lib/gitlab/background_migration/wrongfully_confirmed_email_unconfirmer_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration_spec.rb257
-rw-r--r--spec/lib/gitlab/bare_repository_import/importer_spec.rb2
-rw-r--r--spec/lib/gitlab/bitbucket_server_import/importer_spec.rb74
-rw-r--r--spec/lib/gitlab/blob_helper_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/artifact_file_reader_spec.rb11
-rw-r--r--spec/lib/gitlab/ci/artifacts/metrics_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/build/auto_retry_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb28
-rw-r--r--spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb16
-rw-r--r--spec/lib/gitlab/ci/config/entry/processable_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/config/extendable_spec.rb44
-rw-r--r--spec/lib/gitlab/ci/config/external/processor_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/external/rules_spec.rb28
-rw-r--r--spec/lib/gitlab/ci/config_spec.rb90
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/quota/deployments_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build_spec.rb56
-rw-r--r--spec/lib/gitlab/ci/reports/security/report_spec.rb22
-rw-r--r--spec/lib/gitlab/ci/reports/security/reports_spec.rb21
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb30
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/sast_iac_gitlab_ci_yaml_spec.rb65
-rw-r--r--spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb20
-rw-r--r--spec/lib/gitlab/ci/templates/kaniko_gitlab_ci_yaml_spec.rb25
-rw-r--r--spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/trace/archive_spec.rb169
-rw-r--r--spec/lib/gitlab/ci/trace/metrics_spec.rb18
-rw-r--r--spec/lib/gitlab/ci/trace_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/variables/builder_spec.rb38
-rw-r--r--spec/lib/gitlab/ci/variables/collection_spec.rb482
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb58
-rw-r--r--spec/lib/gitlab/config_checker/external_database_checker_spec.rb6
-rw-r--r--spec/lib/gitlab/container_repository/tags/cache_spec.rb (renamed from spec/services/projects/container_repository/cache_tags_created_at_service_spec.rb)2
-rw-r--r--spec/lib/gitlab/content_security_policy/config_loader_spec.rb49
-rw-r--r--spec/lib/gitlab/contributions_calendar_spec.rb68
-rw-r--r--spec/lib/gitlab/database/async_indexes/postgres_async_index_spec.rb2
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb2
-rw-r--r--spec/lib/gitlab/database/batch_count_spec.rb2
-rw-r--r--spec/lib/gitlab/database/connection_spec.rb442
-rw-r--r--spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb3
-rw-r--r--spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb3
-rw-r--r--spec/lib/gitlab/database/each_database_spec.rb48
-rw-r--r--spec/lib/gitlab/database/gitlab_schema_spec.rb58
-rw-r--r--spec/lib/gitlab/database/load_balancing/configuration_spec.rb75
-rw-r--r--spec/lib/gitlab/database/load_balancing/connection_proxy_spec.rb45
-rw-r--r--spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb102
-rw-r--r--spec/lib/gitlab/database/load_balancing/primary_host_spec.rb6
-rw-r--r--spec/lib/gitlab/database/load_balancing/rack_middleware_spec.rb16
-rw-r--r--spec/lib/gitlab/database/load_balancing/setup_spec.rb208
-rw-r--r--spec/lib/gitlab/database/load_balancing/sidekiq_client_middleware_spec.rb4
-rw-r--r--spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb6
-rw-r--r--spec/lib/gitlab/database/load_balancing/sticking_spec.rb22
-rw-r--r--spec/lib/gitlab/database/load_balancing_spec.rb14
-rw-r--r--spec/lib/gitlab/database/migration_helpers/loose_foreign_key_helpers_spec.rb10
-rw-r--r--spec/lib/gitlab/database/migration_helpers/v2_spec.rb62
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb129
-rw-r--r--spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb23
-rw-r--r--spec/lib/gitlab/database/migrations/observers/transaction_duration_spec.rb106
-rw-r--r--spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb95
-rw-r--r--spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb42
-rw-r--r--spec/lib/gitlab/database/partitioning/multi_database_partition_dropper_spec.rb38
-rw-r--r--spec/lib/gitlab/database/partitioning/multi_database_partition_manager_spec.rb36
-rw-r--r--spec/lib/gitlab/database/partitioning/partition_manager_spec.rb2
-rw-r--r--spec/lib/gitlab/database/partitioning/partition_monitoring_spec.rb3
-rw-r--r--spec/lib/gitlab/database/partitioning/replace_table_spec.rb4
-rw-r--r--spec/lib/gitlab/database/partitioning_spec.rb173
-rw-r--r--spec/lib/gitlab/database/postgres_foreign_key_spec.rb12
-rw-r--r--spec/lib/gitlab/database/postgres_hll/batch_distinct_counter_spec.rb2
-rw-r--r--spec/lib/gitlab/database/postgres_index_bloat_estimate_spec.rb2
-rw-r--r--spec/lib/gitlab/database/postgres_index_spec.rb2
-rw-r--r--spec/lib/gitlab/database/query_analyzer_spec.rb144
-rw-r--r--spec/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics_spec.rb80
-rw-r--r--spec/lib/gitlab/database/query_analyzers/prevent_cross_database_modification_spec.rb (renamed from spec/support_specs/database/prevent_cross_database_modification_spec.rb)112
-rw-r--r--spec/lib/gitlab/database/reflection_spec.rb280
-rw-r--r--spec/lib/gitlab/database/reindexing/index_selection_spec.rb6
-rw-r--r--spec/lib/gitlab/database/reindexing/reindex_action_spec.rb2
-rw-r--r--spec/lib/gitlab/database/reindexing/reindex_concurrently_spec.rb4
-rw-r--r--spec/lib/gitlab/database/reindexing_spec.rb112
-rw-r--r--spec/lib/gitlab/database/schema_cache_with_renamed_table_spec.rb12
-rw-r--r--spec/lib/gitlab/database/schema_migrations/context_spec.rb2
-rw-r--r--spec/lib/gitlab/database/shared_model_spec.rb32
-rw-r--r--spec/lib/gitlab/database/unidirectional_copy_trigger_spec.rb2
-rw-r--r--spec/lib/gitlab/database_spec.rb33
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb42
-rw-r--r--spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb7
-rw-r--r--spec/lib/gitlab/email/handler/service_desk_handler_spec.rb123
-rw-r--r--spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb2
-rw-r--r--spec/lib/gitlab/email/message/in_product_marketing/base_spec.rb25
-rw-r--r--spec/lib/gitlab/email/message/in_product_marketing/experience_spec.rb69
-rw-r--r--spec/lib/gitlab/email/message/in_product_marketing/invite_team_spec.rb39
-rw-r--r--spec/lib/gitlab/email/message/in_product_marketing_spec.rb13
-rw-r--r--spec/lib/gitlab/email/reply_parser_spec.rb24
-rw-r--r--spec/lib/gitlab/emoji_spec.rb106
-rw-r--r--spec/lib/gitlab/etag_caching/middleware_spec.rb2
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb8
-rw-r--r--spec/lib/gitlab/git/object_pool_spec.rb2
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb55
-rw-r--r--spec/lib/gitlab/gitaly_client/commit_service_spec.rb37
-rw-r--r--spec/lib/gitlab/gitaly_client/ref_service_spec.rb51
-rw-r--r--spec/lib/gitlab/gitaly_client_spec.rb23
-rw-r--r--spec/lib/gitlab/github_import/bulk_importing_spec.rb8
-rw-r--r--spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb298
-rw-r--r--spec/lib/gitlab/github_import/importer/diff_notes_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/issue_importer_spec.rb4
-rw-r--r--spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb8
-rw-r--r--spec/lib/gitlab/github_import/importer/note_importer_spec.rb14
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_requests_merged_by_importer_spec.rb25
-rw-r--r--spec/lib/gitlab/github_import/representation/diff_note_spec.rb446
-rw-r--r--spec/lib/gitlab/github_import/representation/diff_notes/suggestion_formatter_spec.rb50
-rw-r--r--spec/lib/gitlab/gpg/commit_spec.rb69
-rw-r--r--spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb2
-rw-r--r--spec/lib/gitlab/grape_logging/loggers/perf_logger_spec.rb2
-rw-r--r--spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb4
-rw-r--r--spec/lib/gitlab/grape_logging/loggers/urgency_logger_spec.rb48
-rw-r--r--spec/lib/gitlab/graphql/known_operations_spec.rb80
-rw-r--r--spec/lib/gitlab/graphql/pagination/connections_spec.rb6
-rw-r--r--spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb15
-rw-r--r--spec/lib/gitlab/graphql/tracers/application_context_tracer_spec.rb43
-rw-r--r--spec/lib/gitlab/graphql/tracers/logger_tracer_spec.rb52
-rw-r--r--spec/lib/gitlab/graphql/tracers/metrics_tracer_spec.rb60
-rw-r--r--spec/lib/gitlab/graphql/tracers/timer_tracer_spec.rb44
-rw-r--r--spec/lib/gitlab/health_checks/redis/redis_check_spec.rb2
-rw-r--r--spec/lib/gitlab/import/database_helpers_spec.rb4
-rw-r--r--spec/lib/gitlab/import/metrics_spec.rb14
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml3
-rw-r--r--spec/lib/gitlab/import_export/attributes_permitter_spec.rb83
-rw-r--r--spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/group/relation_tree_restorer_spec.rb88
-rw-r--r--spec/lib/gitlab/import_export/project/object_builder_spec.rb132
-rw-r--r--spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb150
-rw-r--r--spec/lib/gitlab/import_export/project/sample/relation_tree_restorer_spec.rb48
-rw-r--r--spec/lib/gitlab/import_export/project/tree_restorer_spec.rb3
-rw-r--r--spec/lib/gitlab/import_export/project/tree_saver_spec.rb50
-rw-r--r--spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb184
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml2
-rw-r--r--spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb41
-rw-r--r--spec/lib/gitlab/instrumentation_helper_spec.rb19
-rw-r--r--spec/lib/gitlab/issues/rebalancing/state_spec.rb29
-rw-r--r--spec/lib/gitlab/lograge/custom_options_spec.rb20
-rw-r--r--spec/lib/gitlab/merge_requests/merge_commit_message_spec.rb219
-rw-r--r--spec/lib/gitlab/metrics/background_transaction_spec.rb47
-rw-r--r--spec/lib/gitlab/metrics/method_call_spec.rb4
-rw-r--r--spec/lib/gitlab/metrics/rails_slis_spec.rb37
-rw-r--r--spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb70
-rw-r--r--spec/lib/gitlab/metrics/samplers/action_cable_sampler_spec.rb68
-rw-r--r--spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb4
-rw-r--r--spec/lib/gitlab/metrics/subscribers/external_http_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/transaction_spec.rb167
-rw-r--r--spec/lib/gitlab/metrics/web_transaction_spec.rb90
-rw-r--r--spec/lib/gitlab/middleware/compressed_json_spec.rb75
-rw-r--r--spec/lib/gitlab/middleware/go_spec.rb16
-rw-r--r--spec/lib/gitlab/middleware/query_analyzer_spec.rb61
-rw-r--r--spec/lib/gitlab/path_regex_spec.rb11
-rw-r--r--spec/lib/gitlab/project_template_spec.rb4
-rw-r--r--spec/lib/gitlab/prometheus_client_spec.rb36
-rw-r--r--spec/lib/gitlab/redis/multi_store_spec.rb474
-rw-r--r--spec/lib/gitlab/runtime_spec.rb24
-rw-r--r--spec/lib/gitlab/search_results_spec.rb12
-rw-r--r--spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb15
-rw-r--r--spec/lib/gitlab/sidekiq_config/worker_spec.rb17
-rw-r--r--spec/lib/gitlab/sidekiq_enq_spec.rb93
-rw-r--r--spec/lib/gitlab/sidekiq_logging/deduplication_logger_spec.rb30
-rw-r--r--spec/lib/gitlab/sidekiq_logging/json_formatter_spec.rb2
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb285
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/until_executed_spec.rb25
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/query_analyzer_spec.rb61
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/size_limiter/validator_spec.rb157
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb10
-rw-r--r--spec/lib/gitlab/spamcheck/client_spec.rb2
-rw-r--r--spec/lib/gitlab/subscription_portal_spec.rb31
-rw-r--r--spec/lib/gitlab/tracking/destinations/product_analytics_spec.rb84
-rw-r--r--spec/lib/gitlab/tracking/destinations/snowplow_micro_spec.rb51
-rw-r--r--spec/lib/gitlab/tracking/standard_context_spec.rb20
-rw-r--r--spec/lib/gitlab/tracking_spec.rb89
-rw-r--r--spec/lib/gitlab/usage/metric_definition_spec.rb2
-rw-r--r--spec/lib/gitlab/usage/metric_spec.rb6
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/generic_metric_spec.rb12
-rw-r--r--spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb28
-rw-r--r--spec/lib/gitlab/usage_data_counters/vscode_extenion_activity_unique_counter_spec.rb (renamed from spec/lib/gitlab/usage_data_counters/vs_code_extenion_activity_unique_counter_spec.rb)0
-rw-r--r--spec/lib/gitlab/usage_data_metrics_spec.rb16
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb191
-rw-r--r--spec/lib/gitlab/utils/usage_data_spec.rb26
-rw-r--r--spec/lib/gitlab/webpack/file_loader_spec.rb79
-rw-r--r--spec/lib/gitlab/webpack/graphql_known_operations_spec.rb47
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb18
-rw-r--r--spec/lib/gitlab/x509/certificate_spec.rb50
-rw-r--r--spec/lib/gitlab/x509/signature_spec.rb92
-rw-r--r--spec/lib/gitlab/zentao/client_spec.rb70
-rw-r--r--spec/lib/gitlab/zentao/query_spec.rb61
-rw-r--r--spec/lib/marginalia_spec.rb6
-rw-r--r--spec/lib/object_storage/config_spec.rb40
-rw-r--r--spec/lib/object_storage/direct_upload_spec.rb4
-rw-r--r--spec/lib/security/ci_configuration/sast_iac_build_action_spec.rb163
-rw-r--r--spec/lib/sidebars/groups/menus/invite_team_members_menu_spec.rb55
-rw-r--r--spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb23
-rw-r--r--spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb16
-rw-r--r--spec/lib/sidebars/projects/menus/invite_team_members_menu_spec.rb52
-rw-r--r--spec/lib/sidebars/projects/menus/settings_menu_spec.rb20
-rw-r--r--spec/lib/sidebars/projects/menus/zentao_menu_spec.rb7
-rw-r--r--spec/lib/system_check/incoming_email_check_spec.rb4
-rw-r--r--spec/lib/uploaded_file_spec.rb64
-rw-r--r--spec/mailers/emails/in_product_marketing_spec.rb49
-rw-r--r--spec/mailers/emails/pipelines_spec.rb21
-rw-r--r--spec/mailers/notify_spec.rb87
-rw-r--r--spec/migrations/20200107172020_add_timestamp_softwarelicensespolicy_spec.rb23
-rw-r--r--spec/migrations/20200122123016_backfill_project_settings_spec.rb32
-rw-r--r--spec/migrations/20200123155929_remove_invalid_jira_data_spec.rb77
-rw-r--r--spec/migrations/20200127090233_remove_invalid_issue_tracker_data_spec.rb64
-rw-r--r--spec/migrations/20200130145430_reschedule_migrate_issue_trackers_data_spec.rb115
-rw-r--r--spec/migrations/20200313203550_remove_orphaned_chat_names_spec.rb27
-rw-r--r--spec/migrations/20200406102120_backfill_deployment_clusters_from_deployments_spec.rb50
-rw-r--r--spec/migrations/20200511145545_change_variable_interpolation_format_in_common_metrics_spec.rb39
-rw-r--r--spec/migrations/20200526115436_dedup_mr_metrics_spec.rb68
-rw-r--r--spec/migrations/20200526231421_update_index_approval_rule_name_for_code_owners_rule_type_spec.rb175
-rw-r--r--spec/migrations/20200703125016_backfill_namespace_settings_spec.rb30
-rw-r--r--spec/migrations/20200706035141_adjust_unique_index_alert_management_alerts_spec.rb57
-rw-r--r--spec/migrations/20200728080250_replace_unique_index_on_cycle_analytics_stages_spec.rb47
-rw-r--r--spec/migrations/20200728182311_add_o_auth_paths_to_protected_paths_spec.rb52
-rw-r--r--spec/migrations/20200811130433_create_missing_vulnerabilities_issue_links_spec.rb160
-rw-r--r--spec/migrations/20200915044225_schedule_migration_to_hashed_storage_spec.rb14
-rw-r--r--spec/migrations/20200929052138_create_initial_versions_for_pre_versioning_terraform_states_spec.rb46
-rw-r--r--spec/migrations/20201014205300_drop_backfill_jira_tracker_deployment_type_jobs_spec.rb58
-rw-r--r--spec/migrations/20201027002551_migrate_services_to_http_integrations_spec.rb26
-rw-r--r--spec/migrations/20201028182809_backfill_jira_tracker_deployment_type2_spec.rb38
-rw-r--r--spec/migrations/20201110161542_cleanup_transfered_projects_shared_runners_spec.rb32
-rw-r--r--spec/migrations/20201112130715_schedule_recalculate_uuid_on_vulnerabilities_occurrences_spec.rb138
-rw-r--r--spec/migrations/20210112143418_remove_duplicate_services2_spec.rb2
-rw-r--r--spec/migrations/20210119122354_alter_vsa_issue_first_mentioned_in_commit_value_spec.rb2
-rw-r--r--spec/migrations/20210205174154_remove_bad_dependency_proxy_manifests_spec.rb2
-rw-r--r--spec/migrations/20210210093901_backfill_updated_at_after_repository_storage_move_spec.rb2
-rw-r--r--spec/migrations/20210218040814_add_environment_scope_to_group_variables_spec.rb2
-rw-r--r--spec/migrations/20210226141517_dedup_issue_metrics_spec.rb2
-rw-r--r--spec/migrations/20210406144743_backfill_total_tuple_count_for_batched_migrations_spec.rb2
-rw-r--r--spec/migrations/20210413132500_reschedule_artifact_expiry_backfill_again_spec.rb2
-rw-r--r--spec/migrations/20210421163509_schedule_update_jira_tracker_data_deployment_type_based_on_url_spec.rb2
-rw-r--r--spec/migrations/20210423160427_schedule_drop_invalid_vulnerabilities_spec.rb2
-rw-r--r--spec/migrations/20210430134202_copy_adoption_snapshot_namespace_spec.rb2
-rw-r--r--spec/migrations/20210430135954_copy_adoption_segments_namespace_spec.rb2
-rw-r--r--spec/migrations/20210503105845_add_project_value_stream_id_to_project_stages_spec.rb2
-rw-r--r--spec/migrations/20210511142748_schedule_drop_invalid_vulnerabilities2_spec.rb2
-rw-r--r--spec/migrations/20210514063252_schedule_cleanup_orphaned_lfs_objects_projects_spec.rb2
-rw-r--r--spec/migrations/20210601073400_fix_total_stage_in_vsa_spec.rb2
-rw-r--r--spec/migrations/20210601080039_group_protected_environments_add_index_and_constraint_spec.rb2
-rw-r--r--spec/migrations/20210603222333_remove_builds_email_service_from_services_spec.rb2
-rw-r--r--spec/migrations/20210610153556_delete_legacy_operations_feature_flags_spec.rb2
-rw-r--r--spec/migrations/2021061716138_cascade_delete_freeze_periods_spec.rb2
-rw-r--r--spec/migrations/20210708130419_reschedule_merge_request_diff_users_background_migration_spec.rb2
-rw-r--r--spec/migrations/20210722042939_update_issuable_slas_where_issue_closed_spec.rb2
-rw-r--r--spec/migrations/20210722150102_operations_feature_flags_correct_flexible_rollout_values_spec.rb2
-rw-r--r--spec/migrations/20210804150320_create_base_work_item_types_spec.rb2
-rw-r--r--spec/migrations/20210805192450_update_trial_plans_ci_daily_pipeline_schedule_triggers_spec.rb2
-rw-r--r--spec/migrations/20210811122206_update_external_project_bots_spec.rb2
-rw-r--r--spec/migrations/20210818185845_backfill_projects_with_coverage_spec.rb2
-rw-r--r--spec/migrations/20210819145000_drop_temporary_columns_and_triggers_for_ci_builds_runner_session_spec.rb2
-rw-r--r--spec/migrations/20210831203408_upsert_base_work_item_types_spec.rb2
-rw-r--r--spec/migrations/20210902144144_drop_temporary_columns_and_triggers_for_ci_build_needs_spec.rb2
-rw-r--r--spec/migrations/20210906100316_drop_temporary_columns_and_triggers_for_ci_build_trace_chunks_spec.rb2
-rw-r--r--spec/migrations/20210906130643_drop_temporary_columns_and_triggers_for_taggings_spec.rb2
-rw-r--r--spec/migrations/20210907013944_cleanup_bigint_conversion_for_ci_builds_metadata_spec.rb2
-rw-r--r--spec/migrations/20210907211557_finalize_ci_builds_bigint_conversion_spec.rb2
-rw-r--r--spec/migrations/20210910194952_update_report_type_for_existing_approval_project_rules_spec.rb2
-rw-r--r--spec/migrations/20210914095310_cleanup_orphan_project_access_tokens_spec.rb2
-rw-r--r--spec/migrations/20210915022415_cleanup_bigint_conversion_for_ci_builds_spec.rb2
-rw-r--r--spec/migrations/20210922021816_drop_int4_columns_for_ci_job_artifacts_spec.rb2
-rw-r--r--spec/migrations/20210922025631_drop_int4_column_for_ci_sources_pipelines_spec.rb2
-rw-r--r--spec/migrations/20210922082019_drop_int4_column_for_events_spec.rb2
-rw-r--r--spec/migrations/20210922091402_drop_int4_column_for_push_event_payloads_spec.rb2
-rw-r--r--spec/migrations/20211006060436_schedule_populate_topics_total_projects_count_cache_spec.rb2
-rw-r--r--spec/migrations/20211012134316_clean_up_migrate_merge_request_diff_commit_users_spec.rb48
-rw-r--r--spec/migrations/20211018152654_schedule_remove_duplicate_vulnerabilities_findings3_spec.rb (renamed from spec/migrations/20201112130710_schedule_remove_duplicate_vulnerabilities_findings_spec.rb)76
-rw-r--r--spec/migrations/20211028155449_schedule_fix_merge_request_diff_commit_users_migration_spec.rb63
-rw-r--r--spec/migrations/add_default_value_stream_to_groups_with_group_stages_spec.rb44
-rw-r--r--spec/migrations/add_deploy_token_type_to_deploy_tokens_spec.rb24
-rw-r--r--spec/migrations/add_incident_settings_to_all_existing_projects_spec.rb93
-rw-r--r--spec/migrations/add_open_source_plan_spec.rb86
-rw-r--r--spec/migrations/add_partial_index_to_ci_builds_table_on_user_id_name_spec.rb22
-rw-r--r--spec/migrations/add_repository_storages_weighted_to_application_settings_spec.rb31
-rw-r--r--spec/migrations/add_temporary_partial_index_on_project_id_to_services_spec.rb22
-rw-r--r--spec/migrations/backfill_imported_snippet_repositories_spec.rb52
-rw-r--r--spec/migrations/backfill_operations_feature_flags_iid_spec.rb32
-rw-r--r--spec/migrations/backfill_snippet_repositories_spec.rb44
-rw-r--r--spec/migrations/backfill_status_page_published_incidents_spec.rb54
-rw-r--r--spec/migrations/backfill_user_namespace_spec.rb29
-rw-r--r--spec/migrations/cap_designs_filename_length_to_new_limit_spec.rb62
-rw-r--r--spec/migrations/clean_grafana_url_spec.rb37
-rw-r--r--spec/migrations/cleanup_empty_commit_user_mentions_spec.rb36
-rw-r--r--spec/migrations/cleanup_group_import_states_with_null_user_id_spec.rb101
-rw-r--r--spec/migrations/cleanup_move_container_registry_enabled_to_project_feature_spec.rb (renamed from spec/migrations/cleanup_move_container_registry_enabled_to_project_features_spec.rb)2
-rw-r--r--spec/migrations/cleanup_optimistic_locking_nulls_pt2_fixed_spec.rb45
-rw-r--r--spec/migrations/cleanup_optimistic_locking_nulls_spec.rb52
-rw-r--r--spec/migrations/cleanup_projects_with_missing_namespace_spec.rb142
-rw-r--r--spec/migrations/cleanup_remaining_orphan_invites_spec.rb2
-rw-r--r--spec/migrations/complete_namespace_settings_migration_spec.rb24
-rw-r--r--spec/migrations/confirm_project_bot_users_spec.rb84
-rw-r--r--spec/migrations/create_environment_for_self_monitoring_project_spec.rb68
-rw-r--r--spec/migrations/deduplicate_epic_iids_spec.rb36
-rw-r--r--spec/migrations/delete_internal_ids_where_feature_flags_usage_spec.rb42
-rw-r--r--spec/migrations/delete_template_project_services_spec.rb21
-rw-r--r--spec/migrations/delete_template_services_duplicated_by_type_spec.rb24
-rw-r--r--spec/migrations/delete_user_callout_alerts_moved_spec.rb30
-rw-r--r--spec/migrations/drop_activate_prometheus_services_background_jobs_spec.rb89
-rw-r--r--spec/migrations/drop_background_migration_jobs_spec.rb61
-rw-r--r--spec/migrations/ensure_filled_external_diff_store_on_merge_request_diffs_spec.rb40
-rw-r--r--spec/migrations/ensure_filled_file_store_on_package_files_spec.rb40
-rw-r--r--spec/migrations/ensure_namespace_settings_creation_spec.rb44
-rw-r--r--spec/migrations/ensure_target_project_id_is_filled_spec.rb30
-rw-r--r--spec/migrations/ensure_u2f_registrations_migrated_spec.rb41
-rw-r--r--spec/migrations/fill_file_store_ci_job_artifacts_spec.rb44
-rw-r--r--spec/migrations/fill_file_store_lfs_objects_spec.rb36
-rw-r--r--spec/migrations/fill_store_uploads_spec.rb48
-rw-r--r--spec/migrations/fix_projects_without_project_feature_spec.rb42
-rw-r--r--spec/migrations/fix_projects_without_prometheus_services_spec.rb42
-rw-r--r--spec/migrations/generate_ci_jwt_signing_key_spec.rb42
-rw-r--r--spec/migrations/generate_missing_routes_for_bots_spec.rb80
-rw-r--r--spec/migrations/insert_daily_invites_plan_limits_spec.rb55
-rw-r--r--spec/migrations/insert_project_feature_flags_plan_limits_spec.rb76
-rw-r--r--spec/migrations/migrate_all_merge_request_user_mentions_to_db_spec.rb35
-rw-r--r--spec/migrations/migrate_bot_type_to_user_type_spec.rb20
-rw-r--r--spec/migrations/migrate_commit_notes_mentions_to_db_spec.rb37
-rw-r--r--spec/migrations/migrate_compliance_framework_enum_to_database_framework_record_spec.rb52
-rw-r--r--spec/migrations/migrate_create_commit_signature_worker_sidekiq_queue_spec.rb44
-rw-r--r--spec/migrations/migrate_incident_issues_to_incident_type_spec.rb55
-rw-r--r--spec/migrations/migrate_merge_request_mentions_to_db_spec.rb31
-rw-r--r--spec/migrations/migrate_store_security_reports_sidekiq_queue_spec.rb33
-rw-r--r--spec/migrations/migrate_sync_security_reports_to_report_approval_rules_sidekiq_queue_spec.rb33
-rw-r--r--spec/migrations/orphaned_invite_tokens_cleanup_spec.rb2
-rw-r--r--spec/migrations/populate_remaining_missing_dismissal_information_for_vulnerabilities_spec.rb31
-rw-r--r--spec/migrations/remove_additional_application_settings_rows_spec.rb27
-rw-r--r--spec/migrations/remove_deprecated_jenkins_service_records_spec.rb29
-rw-r--r--spec/migrations/remove_duplicate_labels_from_groups_spec.rb227
-rw-r--r--spec/migrations/remove_duplicate_labels_from_project_spec.rb239
-rw-r--r--spec/migrations/remove_gitlab_issue_tracker_service_records_spec.rb19
-rw-r--r--spec/migrations/remove_orphan_service_hooks_spec.rb26
-rw-r--r--spec/migrations/remove_orphaned_invited_members_spec.rb57
-rw-r--r--spec/migrations/remove_packages_deprecated_dependencies_spec.rb30
-rw-r--r--spec/migrations/remove_security_dashboard_feature_flag_spec.rb53
-rw-r--r--spec/migrations/rename_security_dashboard_feature_flag_to_instance_security_dashboard_spec.rb53
-rw-r--r--spec/migrations/rename_sitemap_namespace_spec.rb30
-rw-r--r--spec/migrations/rename_sitemap_root_namespaces_spec.rb36
-rw-r--r--spec/migrations/reschedule_set_default_iteration_cadences_spec.rb41
-rw-r--r--spec/migrations/reseed_merge_trains_enabled_spec.rb26
-rw-r--r--spec/migrations/reseed_repository_storages_weighted_spec.rb43
-rw-r--r--spec/migrations/save_instance_administrators_group_id_spec.rb99
-rw-r--r--spec/migrations/schedule_add_primary_email_to_emails_if_user_confirmed_spec.rb31
-rw-r--r--spec/migrations/schedule_backfill_push_rules_id_in_projects_spec.rb49
-rw-r--r--spec/migrations/schedule_blocked_by_links_replacement_second_try_spec.rb37
-rw-r--r--spec/migrations/schedule_link_lfs_objects_projects_spec.rb76
-rw-r--r--spec/migrations/schedule_merge_request_cleanup_schedules_backfill_spec.rb41
-rw-r--r--spec/migrations/schedule_migrate_security_scans_spec.rb67
-rw-r--r--spec/migrations/schedule_migrate_u2f_webauthn_spec.rb58
-rw-r--r--spec/migrations/schedule_populate_has_vulnerabilities_spec.rb36
-rw-r--r--spec/migrations/schedule_populate_issue_email_participants_spec.rb33
-rw-r--r--spec/migrations/schedule_populate_missing_dismissal_information_for_vulnerabilities_spec.rb37
-rw-r--r--spec/migrations/schedule_populate_personal_snippet_statistics_spec.rb60
-rw-r--r--spec/migrations/schedule_populate_project_snippet_statistics_spec.rb61
-rw-r--r--spec/migrations/schedule_populate_user_highest_roles_table_spec.rb46
-rw-r--r--spec/migrations/schedule_recalculate_project_authorizations_second_run_spec.rb28
-rw-r--r--spec/migrations/schedule_recalculate_project_authorizations_spec.rb57
-rw-r--r--spec/migrations/schedule_recalculate_project_authorizations_third_run_spec.rb28
-rw-r--r--spec/migrations/schedule_repopulate_historical_vulnerability_statistics_spec.rb36
-rw-r--r--spec/migrations/schedule_update_existing_subgroup_to_match_visibility_level_of_parent_spec.rb79
-rw-r--r--spec/migrations/schedule_update_existing_users_that_require_two_factor_auth_spec.rb29
-rw-r--r--spec/migrations/seed_merge_trains_enabled_spec.rb28
-rw-r--r--spec/migrations/seed_repository_storages_weighted_spec.rb31
-rw-r--r--spec/migrations/services_remove_temporary_index_on_project_id_spec.rb40
-rw-r--r--spec/migrations/set_job_waiter_ttl_spec.rb30
-rw-r--r--spec/migrations/slice_merge_request_diff_commit_migrations_spec.rb2
-rw-r--r--spec/migrations/steal_merge_request_diff_commit_users_migration_spec.rb2
-rw-r--r--spec/migrations/unconfirm_wrongfully_verified_emails_spec.rb55
-rw-r--r--spec/migrations/update_application_setting_npm_package_requests_forwarding_default_spec.rb38
-rw-r--r--spec/migrations/update_fingerprint_sha256_within_keys_spec.rb30
-rw-r--r--spec/migrations/update_historical_data_recorded_at_spec.rb31
-rw-r--r--spec/migrations/update_internal_ids_last_value_for_epics_renamed_spec.rb30
-rw-r--r--spec/migrations/update_routes_for_lost_and_found_group_and_orphaned_projects_spec.rb223
-rw-r--r--spec/migrations/update_timestamp_softwarelicensespolicy_spec.rb24
-rw-r--r--spec/models/ability_spec.rb4
-rw-r--r--spec/models/acts_as_taggable_on/tag_spec.rb16
-rw-r--r--spec/models/acts_as_taggable_on/tagging_spec.rb16
-rw-r--r--spec/models/analytics/cycle_analytics/issue_stage_event_spec.rb9
-rw-r--r--spec/models/analytics/cycle_analytics/merge_request_stage_event_spec.rb9
-rw-r--r--spec/models/blob_viewer/package_json_spec.rb52
-rw-r--r--spec/models/bulk_imports/entity_spec.rb9
-rw-r--r--spec/models/bulk_imports/file_transfer/project_config_spec.rb2
-rw-r--r--spec/models/chat_name_spec.rb8
-rw-r--r--spec/models/ci/bridge_spec.rb2
-rw-r--r--spec/models/ci/build_metadata_spec.rb12
-rw-r--r--spec/models/ci/build_spec.rb267
-rw-r--r--spec/models/ci/job_artifact_spec.rb62
-rw-r--r--spec/models/ci/pipeline_schedule_spec.rb2
-rw-r--r--spec/models/ci/pipeline_spec.rb18
-rw-r--r--spec/models/ci/runner_spec.rb53
-rw-r--r--spec/models/ci/trigger_spec.rb4
-rw-r--r--spec/models/clusters/applications/runner_spec.rb6
-rw-r--r--spec/models/clusters/cluster_spec.rb6
-rw-r--r--spec/models/commit_status_spec.rb16
-rw-r--r--spec/models/concerns/bulk_insert_safe_spec.rb24
-rw-r--r--spec/models/concerns/bulk_insertable_associations_spec.rb32
-rw-r--r--spec/models/concerns/cascading_namespace_setting_attribute_spec.rb15
-rw-r--r--spec/models/concerns/clusters/agents/authorization_config_scopes_spec.rb21
-rw-r--r--spec/models/concerns/database_reflection_spec.rb18
-rw-r--r--spec/models/concerns/has_integrations_spec.rb25
-rw-r--r--spec/models/concerns/legacy_bulk_insert_spec.rb103
-rw-r--r--spec/models/concerns/loaded_in_group_list_spec.rb58
-rw-r--r--spec/models/concerns/loose_foreign_key_spec.rb29
-rw-r--r--spec/models/concerns/noteable_spec.rb64
-rw-r--r--spec/models/concerns/prometheus_adapter_spec.rb8
-rw-r--r--spec/models/concerns/reactive_caching_spec.rb2
-rw-r--r--spec/models/concerns/sha256_attribute_spec.rb2
-rw-r--r--spec/models/concerns/sha_attribute_spec.rb2
-rw-r--r--spec/models/concerns/where_composite_spec.rb2
-rw-r--r--spec/models/concerns/x509_serial_number_attribute_spec.rb2
-rw-r--r--spec/models/custom_emoji_spec.rb2
-rw-r--r--spec/models/customer_relations/contact_spec.rb3
-rw-r--r--spec/models/customer_relations/issue_contact_spec.rb48
-rw-r--r--spec/models/data_list_spec.rb31
-rw-r--r--spec/models/dependency_proxy/manifest_spec.rb21
-rw-r--r--spec/models/deploy_key_spec.rb11
-rw-r--r--spec/models/deployment_spec.rb81
-rw-r--r--spec/models/design_management/version_spec.rb2
-rw-r--r--spec/models/email_spec.rb11
-rw-r--r--spec/models/environment_spec.rb44
-rw-r--r--spec/models/error_tracking/error_event_spec.rb20
-rw-r--r--spec/models/error_tracking/error_spec.rb4
-rw-r--r--spec/models/event_spec.rb12
-rw-r--r--spec/models/fork_network_spec.rb6
-rw-r--r--spec/models/generic_commit_status_spec.rb2
-rw-r--r--spec/models/grafana_integration_spec.rb6
-rw-r--r--spec/models/group_spec.rb55
-rw-r--r--spec/models/hooks/project_hook_spec.rb2
-rw-r--r--spec/models/identity_spec.rb10
-rw-r--r--spec/models/integration_spec.rb18
-rw-r--r--spec/models/integrations/jira_spec.rb148
-rw-r--r--spec/models/integrations/pipelines_email_spec.rb38
-rw-r--r--spec/models/integrations/shimo_spec.rb41
-rw-r--r--spec/models/integrations/zentao_spec.rb6
-rw-r--r--spec/models/issue_spec.rb25
-rw-r--r--spec/models/jira_import_state_spec.rb4
-rw-r--r--spec/models/key_spec.rb4
-rw-r--r--spec/models/loose_foreign_keys/deleted_record_spec.rb35
-rw-r--r--spec/models/loose_foreign_keys/modification_tracker_spec.rb93
-rw-r--r--spec/models/member_spec.rb14
-rw-r--r--spec/models/members/member_task_spec.rb124
-rw-r--r--spec/models/members/project_member_spec.rb54
-rw-r--r--spec/models/merge_request_assignee_spec.rb4
-rw-r--r--spec/models/merge_request_diff_commit_spec.rb16
-rw-r--r--spec/models/merge_request_diff_spec.rb20
-rw-r--r--spec/models/merge_request_reviewer_spec.rb4
-rw-r--r--spec/models/merge_request_spec.rb22
-rw-r--r--spec/models/namespace_spec.rb295
-rw-r--r--spec/models/namespaces/project_namespace_spec.rb2
-rw-r--r--spec/models/note_spec.rb22
-rw-r--r--spec/models/notification_setting_spec.rb12
-rw-r--r--spec/models/operations/feature_flags/strategy_spec.rb269
-rw-r--r--spec/models/operations/feature_flags/user_list_spec.rb21
-rw-r--r--spec/models/packages/npm/metadatum_spec.rb50
-rw-r--r--spec/models/packages/package_file_spec.rb23
-rw-r--r--spec/models/packages/package_spec.rb24
-rw-r--r--spec/models/pages_domain_spec.rb4
-rw-r--r--spec/models/preloaders/group_policy_preloader_spec.rb45
-rw-r--r--spec/models/preloaders/group_root_ancestor_preloader_spec.rb63
-rw-r--r--spec/models/preloaders/user_max_access_level_in_groups_preloader_spec.rb49
-rw-r--r--spec/models/project_authorization_spec.rb21
-rw-r--r--spec/models/project_spec.rb113
-rw-r--r--spec/models/project_statistics_spec.rb4
-rw-r--r--spec/models/project_team_spec.rb14
-rw-r--r--spec/models/protectable_dropdown_spec.rb2
-rw-r--r--spec/models/redirect_route_spec.rb10
-rw-r--r--spec/models/release_spec.rb14
-rw-r--r--spec/models/remote_mirror_spec.rb10
-rw-r--r--spec/models/repository_spec.rb79
-rw-r--r--spec/models/route_spec.rb14
-rw-r--r--spec/models/sentry_issue_spec.rb2
-rw-r--r--spec/models/snippet_spec.rb6
-rw-r--r--spec/models/suggestion_spec.rb8
-rw-r--r--spec/models/u2f_registration_spec.rb28
-rw-r--r--spec/models/upload_spec.rb18
-rw-r--r--spec/models/uploads/fog_spec.rb27
-rw-r--r--spec/models/user_spec.rb138
-rw-r--r--spec/models/users/credit_card_validation_spec.rb18
-rw-r--r--spec/models/users/in_product_marketing_email_spec.rb3
-rw-r--r--spec/models/users/merge_request_interaction_spec.rb5
-rw-r--r--spec/models/users_statistics_spec.rb16
-rw-r--r--spec/models/webauthn_registration_spec.rb23
-rw-r--r--spec/policies/group_policy_spec.rb33
-rw-r--r--spec/policies/namespaces/project_namespace_policy_spec.rb3
-rw-r--r--spec/policies/project_policy_spec.rb84
-rw-r--r--spec/presenters/award_emoji_presenter_spec.rb9
-rw-r--r--spec/presenters/blob_presenter_spec.rb55
-rw-r--r--spec/presenters/ci/build_runner_presenter_spec.rb57
-rw-r--r--spec/presenters/packages/npm/package_presenter_spec.rb88
-rw-r--r--spec/presenters/project_presenter_spec.rb74
-rw-r--r--spec/presenters/release_presenter_spec.rb14
-rw-r--r--spec/requests/admin/applications_controller_spec.rb18
-rw-r--r--spec/requests/api/api_spec.rb24
-rw-r--r--spec/requests/api/ci/jobs_spec.rb173
-rw-r--r--spec/requests/api/ci/runner/jobs_request_post_spec.rb6
-rw-r--r--spec/requests/api/debian_group_packages_spec.rb15
-rw-r--r--spec/requests/api/debian_project_packages_spec.rb21
-rw-r--r--spec/requests/api/deploy_keys_spec.rb54
-rw-r--r--spec/requests/api/error_tracking/collector_spec.rb51
-rw-r--r--spec/requests/api/features_spec.rb30
-rw-r--r--spec/requests/api/files_spec.rb44
-rw-r--r--spec/requests/api/generic_packages_spec.rb31
-rw-r--r--spec/requests/api/graphql/ci/pipelines_spec.rb63
-rw-r--r--spec/requests/api/graphql/gitlab_schema_spec.rb23
-rw-r--r--spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb22
-rw-r--r--spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/design_management/delete_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/issues/create_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/issues/move_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb161
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_severity_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_draft_spec.rb (renamed from spec/requests/api/graphql/mutations/merge_requests/set_wip_spec.rb)8
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/update_reviewer_state_spec.rb65
-rw-r--r--spec/requests/api/graphql/mutations/releases/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/releases/delete_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/releases/update_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/security/ci_configuration/configure_sast_iac_spec.rb26
-rw-r--r--spec/requests/api/graphql/namespace_query_spec.rb86
-rw-r--r--spec/requests/api/graphql/packages/helm_spec.rb59
-rw-r--r--spec/requests/api/graphql/project/issues_spec.rb4
-rw-r--r--spec/requests/api/graphql/project/merge_request_spec.rb16
-rw-r--r--spec/requests/api/graphql/project/release_spec.rb186
-rw-r--r--spec/requests/api/graphql/project/releases_spec.rb15
-rw-r--r--spec/requests/api/graphql_spec.rb30
-rw-r--r--spec/requests/api/group_debian_distributions_spec.rb16
-rw-r--r--spec/requests/api/groups_spec.rb7
-rw-r--r--spec/requests/api/internal/base_spec.rb2
-rw-r--r--spec/requests/api/invitations_spec.rb32
-rw-r--r--spec/requests/api/lint_spec.rb123
-rw-r--r--spec/requests/api/members_spec.rb54
-rw-r--r--spec/requests/api/merge_requests_spec.rb2
-rw-r--r--spec/requests/api/namespaces_spec.rb76
-rw-r--r--spec/requests/api/npm_project_packages_spec.rb20
-rw-r--r--spec/requests/api/project_attributes.yml1
-rw-r--r--spec/requests/api/project_debian_distributions_spec.rb22
-rw-r--r--spec/requests/api/project_import_spec.rb2
-rw-r--r--spec/requests/api/project_snapshots_spec.rb1
-rw-r--r--spec/requests/api/project_snippets_spec.rb1
-rw-r--r--spec/requests/api/projects_spec.rb31
-rw-r--r--spec/requests/api/releases_spec.rb32
-rw-r--r--spec/requests/api/repositories_spec.rb39
-rw-r--r--spec/requests/api/settings_spec.rb41
-rw-r--r--spec/requests/api/snippets_spec.rb1
-rw-r--r--spec/requests/api/tags_spec.rb67
-rw-r--r--spec/requests/api/terraform/modules/v1/packages_spec.rb17
-rw-r--r--spec/requests/api/todos_spec.rb12
-rw-r--r--spec/requests/api/topics_spec.rb217
-rw-r--r--spec/requests/api/users_spec.rb8
-rw-r--r--spec/requests/api/v3/github_spec.rb131
-rw-r--r--spec/requests/groups/email_campaigns_controller_spec.rb10
-rw-r--r--spec/requests/groups/settings/applications_controller_spec.rb20
-rw-r--r--spec/requests/import/gitlab_groups_controller_spec.rb2
-rw-r--r--spec/requests/jwks_controller_spec.rb14
-rw-r--r--spec/requests/oauth/applications_controller_spec.rb18
-rw-r--r--spec/requests/projects/google_cloud_controller_spec.rb94
-rw-r--r--spec/requests/projects/issues/discussions_spec.rb115
-rw-r--r--spec/requests/projects/issues_controller_spec.rb71
-rw-r--r--spec/requests/projects/usage_quotas_spec.rb50
-rw-r--r--spec/requests/rack_attack_global_spec.rb4
-rw-r--r--spec/requests/users_controller_spec.rb2
-rw-r--r--spec/routing/group_routing_spec.rb20
-rw-r--r--spec/routing/openid_connect_spec.rb12
-rw-r--r--spec/rubocop/cop/gitlab/bulk_insert_spec.rb12
-rw-r--r--spec/rubocop/cop/gitlab/change_timezone_spec.rb2
-rw-r--r--spec/rubocop/cop/qa/duplicate_testcase_link_spec.rb36
-rw-r--r--spec/scripts/changed-feature-flags_spec.rb79
-rw-r--r--spec/scripts/failed_tests_spec.rb127
-rw-r--r--spec/scripts/pipeline_test_report_builder_spec.rb185
-rw-r--r--spec/serializers/analytics_summary_serializer_spec.rb2
-rw-r--r--spec/serializers/merge_request_user_entity_spec.rb9
-rw-r--r--spec/serializers/merge_request_widget_entity_spec.rb33
-rw-r--r--spec/serializers/service_field_entity_spec.rb14
-rw-r--r--spec/services/admin/propagate_integration_service_spec.rb4
-rw-r--r--spec/services/authorized_project_update/project_access_changed_service_spec.rb21
-rw-r--r--spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb4
-rw-r--r--spec/services/award_emojis/base_service_spec.rb25
-rw-r--r--spec/services/bulk_create_integration_service_spec.rb21
-rw-r--r--spec/services/bulk_update_integration_service_spec.rb41
-rw-r--r--spec/services/ci/create_pipeline_service/include_spec.rb89
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb4
-rw-r--r--spec/services/ci/external_pull_requests/create_pipeline_service_spec.rb22
-rw-r--r--spec/services/ci/generate_kubeconfig_service_spec.rb50
-rw-r--r--spec/services/ci/job_artifacts/create_service_spec.rb14
-rw-r--r--spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb63
-rw-r--r--spec/services/ci/job_artifacts/destroy_batch_service_spec.rb3
-rw-r--r--spec/services/ci/parse_dotenv_artifact_service_spec.rb6
-rw-r--r--spec/services/ci/retry_build_service_spec.rb31
-rw-r--r--spec/services/ci/unlock_artifacts_service_spec.rb280
-rw-r--r--spec/services/ci/update_build_state_service_spec.rb22
-rw-r--r--spec/services/clusters/agents/refresh_authorization_service_spec.rb10
-rw-r--r--spec/services/clusters/cleanup/project_namespace_service_spec.rb13
-rw-r--r--spec/services/clusters/cleanup/service_account_service_spec.rb8
-rw-r--r--spec/services/clusters/integrations/prometheus_health_check_service_spec.rb (renamed from spec/services/clusters/applications/prometheus_health_check_service_spec.rb)32
-rw-r--r--spec/services/dependency_proxy/find_or_create_blob_service_spec.rb2
-rw-r--r--spec/services/dependency_proxy/find_or_create_manifest_service_spec.rb66
-rw-r--r--spec/services/dependency_proxy/head_manifest_service_spec.rb2
-rw-r--r--spec/services/dependency_proxy/pull_manifest_service_spec.rb2
-rw-r--r--spec/services/deployments/archive_in_project_service_spec.rb80
-rw-r--r--spec/services/deployments/link_merge_requests_service_spec.rb13
-rw-r--r--spec/services/emails/create_service_spec.rb5
-rw-r--r--spec/services/emails/destroy_service_spec.rb10
-rw-r--r--spec/services/error_tracking/collect_error_service_spec.rb31
-rw-r--r--spec/services/google_cloud/service_accounts_service_spec.rb58
-rw-r--r--spec/services/groups/create_service_spec.rb41
-rw-r--r--spec/services/groups/import_export/import_service_spec.rb6
-rw-r--r--spec/services/groups/transfer_service_spec.rb104
-rw-r--r--spec/services/import/github/notes/create_service_spec.rb24
-rw-r--r--spec/services/issues/build_service_spec.rb96
-rw-r--r--spec/services/issues/close_service_spec.rb44
-rw-r--r--spec/services/issues/create_service_spec.rb79
-rw-r--r--spec/services/issues/set_crm_contacts_service_spec.rb162
-rw-r--r--spec/services/issues/update_service_spec.rb42
-rw-r--r--spec/services/labels/transfer_service_spec.rb156
-rw-r--r--spec/services/loose_foreign_keys/batch_cleaner_service_spec.rb119
-rw-r--r--spec/services/loose_foreign_keys/cleaner_service_spec.rb147
-rw-r--r--spec/services/members/create_service_spec.rb104
-rw-r--r--spec/services/members/invite_service_spec.rb5
-rw-r--r--spec/services/merge_requests/mergeability/run_checks_service_spec.rb4
-rw-r--r--spec/services/merge_requests/retarget_chain_service_spec.rb8
-rw-r--r--spec/services/merge_requests/toggle_attention_requested_service_spec.rb128
-rw-r--r--spec/services/namespaces/in_product_marketing_email_records_spec.rb55
-rw-r--r--spec/services/namespaces/invite_team_email_service_spec.rb128
-rw-r--r--spec/services/notification_service_spec.rb24
-rw-r--r--spec/services/packages/create_dependency_service_spec.rb6
-rw-r--r--spec/services/packages/npm/create_package_service_spec.rb46
-rw-r--r--spec/services/packages/update_tags_service_spec.rb2
-rw-r--r--spec/services/projects/all_issues_count_service_spec.rb24
-rw-r--r--spec/services/projects/all_merge_requests_count_service_spec.rb30
-rw-r--r--spec/services/projects/container_repository/cleanup_tags_service_spec.rb457
-rw-r--r--spec/services/projects/create_service_spec.rb35
-rw-r--r--spec/services/projects/destroy_service_spec.rb8
-rw-r--r--spec/services/projects/import_export/export_service_spec.rb12
-rw-r--r--spec/services/projects/lfs_pointers/lfs_download_service_spec.rb1
-rw-r--r--spec/services/projects/participants_service_spec.rb8
-rw-r--r--spec/services/projects/prometheus/alerts/notify_service_spec.rb3
-rw-r--r--spec/services/projects/transfer_service_spec.rb8
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb53
-rw-r--r--spec/services/resource_events/change_labels_service_spec.rb2
-rw-r--r--spec/services/resource_events/synthetic_label_notes_builder_service_spec.rb12
-rw-r--r--spec/services/resource_events/synthetic_milestone_notes_builder_service_spec.rb2
-rw-r--r--spec/services/resource_events/synthetic_state_notes_builder_service_spec.rb11
-rw-r--r--spec/services/security/ci_configuration/sast_iac_create_service_spec.rb19
-rw-r--r--spec/services/spam/spam_verdict_service_spec.rb4
-rw-r--r--spec/services/system_note_service_spec.rb199
-rw-r--r--spec/services/system_notes/incident_service_spec.rb10
-rw-r--r--spec/services/system_notes/issuables_service_spec.rb17
-rw-r--r--spec/services/tasks_to_be_done/base_service_spec.rb69
-rw-r--r--spec/services/todo_service_spec.rb11
-rw-r--r--spec/services/users/update_service_spec.rb2
-rw-r--r--spec/services/users/upsert_credit_card_validation_service_spec.rb13
-rw-r--r--spec/sidekiq_cluster/sidekiq_cluster_spec.rb (renamed from spec/lib/gitlab/sidekiq_cluster_spec.rb)5
-rw-r--r--spec/spec_helper.rb20
-rw-r--r--spec/support/capybara.rb4
-rw-r--r--spec/support/database/cross-database-modification-allowlist.yml1259
-rw-r--r--spec/support/database/cross-join-allowlist.yml54
-rw-r--r--spec/support/database/gitlab_schema.rb25
-rw-r--r--spec/support/database/multiple_databases.rb27
-rw-r--r--spec/support/database/prevent_cross_database_modification.rb122
-rw-r--r--spec/support/database/prevent_cross_joins.rb4
-rw-r--r--spec/support/database/query_analyzer.rb14
-rw-r--r--spec/support/database_load_balancing.rb16
-rw-r--r--spec/support/flaky_tests.rb36
-rw-r--r--spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb4
-rw-r--r--spec/support/graphql/fake_query_type.rb15
-rw-r--r--spec/support/graphql/fake_tracer.rb15
-rw-r--r--spec/support/helpers/cycle_analytics_helpers.rb4
-rw-r--r--spec/support/helpers/features/invite_members_modal_helper.rb2
-rw-r--r--spec/support/helpers/gitaly_setup.rb2
-rw-r--r--spec/support/helpers/gpg_helpers.rb1
-rw-r--r--spec/support/helpers/graphql_helpers.rb3
-rw-r--r--spec/support/helpers/migrations_helpers.rb6
-rw-r--r--spec/support/helpers/navbar_structure_helper.rb13
-rw-r--r--spec/support/helpers/project_forks_helper.rb6
-rw-r--r--spec/support/helpers/require_migration.rb6
-rw-r--r--spec/support/helpers/stub_gitlab_calls.rb9
-rw-r--r--spec/support/helpers/stub_object_storage.rb12
-rw-r--r--spec/support/helpers/test_env.rb4
-rw-r--r--spec/support/helpers/usage_data_helpers.rb2
-rw-r--r--spec/support/helpers/workhorse_helpers.rb7
-rw-r--r--spec/support/matchers/access_matchers.rb2
-rw-r--r--spec/support/matchers/project_namespace_matcher.rb28
-rw-r--r--spec/support/patches/rspec_example_prepended_methods.rb26
-rw-r--r--spec/support/redis/redis_shared_examples.rb37
-rw-r--r--spec/support/retriable.rb7
-rw-r--r--spec/support/shared_contexts/graphql/requests/packages_shared_context.rb6
-rw-r--r--spec/support/shared_contexts/lib/gitlab/database/background_migration_job_shared_context.rb21
-rw-r--r--spec/support/shared_contexts/navbar_structure_context.rb2
-rw-r--r--spec/support/shared_contexts/policies/project_policy_shared_context.rb11
-rw-r--r--spec/support/shared_contexts/requests/api/debian_repository_shared_context.rb120
-rw-r--r--spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb4
-rw-r--r--spec/support/shared_contexts/services/service_ping/stubbed_service_ping_metrics_definitions_shared_context.rb5
-rw-r--r--spec/support/shared_contexts/url_shared_context.rb39
-rw-r--r--spec/support/shared_examples/bulk_imports/common/pipelines/wiki_pipeline_examples.rb31
-rw-r--r--spec/support/shared_examples/controllers/concerns/integrations/integrations_actions_shared_examples.rb (renamed from spec/support/shared_examples/controllers/concerns/integrations_actions_shared_examples.rb)2
-rw-r--r--spec/support/shared_examples/controllers/create_notes_rate_limit_shared_examples.rb58
-rw-r--r--spec/support/shared_examples/features/2fa_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/features/dependency_proxy_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/manage_applications_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/packages_shared_examples.rb30
-rw-r--r--spec/support/shared_examples/features/resolving_discussions_in_issues_shared_examples.rb38
-rw-r--r--spec/support/shared_examples/features/sidebar_shared_examples.rb11
-rw-r--r--spec/support/shared_examples/graphql/notes_creation_shared_examples.rb26
-rw-r--r--spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb67
-rw-r--r--spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb2
-rw-r--r--spec/support/shared_examples/lib/gitlab/database/cte_materialized_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/lib/gitlab/import_export/attributes_permitter_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb44
-rw-r--r--spec/support/shared_examples/lib/sidebars/projects/menus/zentao_menu_shared_examples.rb42
-rw-r--r--spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb52
-rw-r--r--spec/support/shared_examples/metrics/transaction_metrics_with_labels_shared_examples.rb219
-rw-r--r--spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb117
-rw-r--r--spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb15
-rw-r--r--spec/support/shared_examples/models/member_shared_examples.rb31
-rw-r--r--spec/support/shared_examples/models/reviewer_state_shared_examples.rb15
-rw-r--r--spec/support/shared_examples/namespaces/traversal_examples.rb22
-rw-r--r--spec/support/shared_examples/namespaces/traversal_scope_examples.rb81
-rw-r--r--spec/support/shared_examples/quick_actions/issue/promote_to_incident_quick_action_shared_examples.rb40
-rw-r--r--spec/support/shared_examples/requests/api/debian_common_shared_examples.rb17
-rw-r--r--spec/support/shared_examples/requests/api/debian_distributions_shared_examples.rb192
-rw-r--r--spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb369
-rw-r--r--spec/support/shared_examples/requests/api/graphql/mutations/destroy_list_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb18
-rw-r--r--spec/support/shared_examples/requests/api/notes_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb15
-rw-r--r--spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/requests/api/status_shared_examples.rb29
-rw-r--r--spec/support/shared_examples/requests/applications_controller_shared_examples.rb44
-rw-r--r--spec/support/shared_examples/requests/self_monitoring_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/requests/snippet_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/service_desk_issue_templates_examples.rb8
-rw-r--r--spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/services/jira/requests/base_shared_examples.rb11
-rw-r--r--spec/support/shared_examples/services/resource_events/synthetic_notes_builder_shared_examples.rb25
-rw-r--r--spec/support/shared_examples/workers/self_monitoring_shared_examples.rb2
-rw-r--r--spec/support/stub_snowplow.rb2
-rw-r--r--spec/support/test_reports/test_reports_helper.rb6
-rw-r--r--spec/support/time_travel.rb21
-rw-r--r--spec/support_specs/database/multiple_databases_spec.rb6
-rw-r--r--spec/support_specs/helpers/stub_feature_flags_spec.rb2
-rw-r--r--spec/support_specs/time_travel_spec.rb21
-rw-r--r--spec/tasks/gitlab/db_rake_spec.rb46
-rw-r--r--spec/tasks/gitlab/gitaly_rake_spec.rb35
-rw-r--r--spec/tasks/gitlab/storage_rake_spec.rb4
-rw-r--r--spec/tooling/danger/changelog_spec.rb4
-rw-r--r--spec/tooling/danger/product_intelligence_spec.rb83
-rw-r--r--spec/tooling/danger/project_helper_spec.rb128
-rw-r--r--spec/tooling/quality/test_level_spec.rb12
-rw-r--r--spec/validators/addressable_url_validator_spec.rb12
-rw-r--r--spec/views/groups/settings/_remove.html.haml_spec.rb4
-rw-r--r--spec/views/groups/settings/_transfer.html.haml_spec.rb6
-rw-r--r--spec/views/jira_connect/subscriptions/index.html.haml_spec.rb2
-rw-r--r--spec/views/layouts/_published_experiments.html.haml_spec.rb35
-rw-r--r--spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb24
-rw-r--r--spec/views/profiles/audit_log.html.haml_spec.rb26
-rw-r--r--spec/views/projects/edit.html.haml_spec.rb35
-rw-r--r--spec/views/projects/issues/_service_desk_info_content.html.haml_spec.rb95
-rw-r--r--spec/workers/analytics/usage_trends/counter_job_worker_spec.rb3
-rw-r--r--spec/workers/ci/ref_delete_unlock_artifacts_worker_spec.rb34
-rw-r--r--spec/workers/ci/resource_groups/assign_resource_from_resource_group_worker_spec.rb4
-rw-r--r--spec/workers/clusters/integrations/check_prometheus_health_worker_spec.rb (renamed from spec/workers/clusters/applications/check_prometheus_health_worker_spec.rb)8
-rw-r--r--spec/workers/concerns/application_worker_spec.rb381
-rw-r--r--spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb5
-rw-r--r--spec/workers/database/drop_detached_partitions_worker_spec.rb8
-rw-r--r--spec/workers/database/partition_management_worker_spec.rb9
-rw-r--r--spec/workers/dependency_proxy/image_ttl_group_policy_worker_spec.rb4
-rw-r--r--spec/workers/deployments/archive_in_project_worker_spec.rb18
-rw-r--r--spec/workers/email_receiver_worker_spec.rb9
-rw-r--r--spec/workers/emails_on_push_worker_spec.rb37
-rw-r--r--spec/workers/every_sidekiq_worker_spec.rb2
-rw-r--r--spec/workers/integrations/create_external_cross_reference_worker_spec.rb128
-rw-r--r--spec/workers/issue_rebalancing_worker_spec.rb16
-rw-r--r--spec/workers/issues/placement_worker_spec.rb151
-rw-r--r--spec/workers/issues/rebalancing_worker_spec.rb90
-rw-r--r--spec/workers/issues/reschedule_stuck_issue_rebalances_worker_spec.rb26
-rw-r--r--spec/workers/loose_foreign_keys/cleanup_worker_spec.rb153
-rw-r--r--spec/workers/namespaces/invite_team_email_worker_spec.rb27
-rw-r--r--spec/workers/packages/maven/metadata/sync_worker_spec.rb3
-rw-r--r--spec/workers/post_receive_spec.rb8
-rw-r--r--spec/workers/propagate_integration_group_worker_spec.rb2
-rw-r--r--spec/workers/propagate_integration_inherit_descendant_worker_spec.rb4
-rw-r--r--spec/workers/propagate_integration_project_worker_spec.rb2
-rw-r--r--spec/workers/ssh_keys/expired_notification_worker_spec.rb6
-rw-r--r--spec/workers/tasks_to_be_done/create_worker_spec.rb36
-rw-r--r--spec/workers/users/deactivate_dormant_users_worker_spec.rb36
1532 files changed, 39539 insertions, 23552 deletions
diff --git a/spec/lib/gitlab/sidekiq_cluster/cli_spec.rb b/spec/commands/sidekiq_cluster/cli_spec.rb
index e818b03cf75..baa4a2b4ec3 100644
--- a/spec/lib/gitlab/sidekiq_cluster/cli_spec.rb
+++ b/spec/commands/sidekiq_cluster/cli_spec.rb
@@ -3,9 +3,11 @@
require 'fast_spec_helper'
require 'rspec-parameterized'
-RSpec.describe Gitlab::SidekiqCluster::CLI do
+require_relative '../../../sidekiq_cluster/cli'
+
+RSpec.describe Gitlab::SidekiqCluster::CLI do # rubocop:disable RSpec/FilePath
let(:cli) { described_class.new('/dev/null') }
- let(:timeout) { described_class::DEFAULT_SOFT_TIMEOUT_SECONDS }
+ let(:timeout) { Gitlab::SidekiqCluster::DEFAULT_SOFT_TIMEOUT_SECONDS }
let(:default_options) do
{ env: 'test', directory: Dir.pwd, max_concurrency: 50, min_concurrency: 0, dryrun: false, timeout: timeout }
end
@@ -103,7 +105,7 @@ RSpec.describe Gitlab::SidekiqCluster::CLI do
it 'when not given', 'starts Sidekiq workers with default timeout' do
expect(Gitlab::SidekiqCluster).to receive(:start)
- .with([['foo']], default_options.merge(timeout: described_class::DEFAULT_SOFT_TIMEOUT_SECONDS))
+ .with([['foo']], default_options.merge(timeout: Gitlab::SidekiqCluster::DEFAULT_SOFT_TIMEOUT_SECONDS))
cli.run(%w(foo))
end
@@ -271,7 +273,7 @@ RSpec.describe Gitlab::SidekiqCluster::CLI do
expect(Gitlab::SidekiqCluster).to receive(:signal_processes)
.with([], "-KILL")
- stub_const("Gitlab::SidekiqCluster::CLI::CHECK_TERMINATE_INTERVAL_SECONDS", 0.1)
+ stub_const("Gitlab::SidekiqCluster::CHECK_TERMINATE_INTERVAL_SECONDS", 0.1)
allow(cli).to receive(:terminate_timeout_seconds) { 1 }
cli.wait_for_termination
@@ -301,7 +303,7 @@ RSpec.describe Gitlab::SidekiqCluster::CLI do
cli.run(%w(foo))
- stub_const("Gitlab::SidekiqCluster::CLI::CHECK_TERMINATE_INTERVAL_SECONDS", 0.1)
+ stub_const("Gitlab::SidekiqCluster::CHECK_TERMINATE_INTERVAL_SECONDS", 0.1)
allow(cli).to receive(:terminate_timeout_seconds) { 1 }
cli.wait_for_termination
diff --git a/spec/controllers/admin/integrations_controller_spec.rb b/spec/controllers/admin/integrations_controller_spec.rb
index 1793b3a86d1..cf6a6385425 100644
--- a/spec/controllers/admin/integrations_controller_spec.rb
+++ b/spec/controllers/admin/integrations_controller_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Admin::IntegrationsController do
sign_in(admin)
end
- it_behaves_like IntegrationsActions do
+ it_behaves_like Integrations::Actions do
let(:integration_attributes) { { instance: true, project: nil } }
let(:routing_params) do
diff --git a/spec/controllers/admin/runners_controller_spec.rb b/spec/controllers/admin/runners_controller_spec.rb
index 996964fdcf0..b9a59e9ae5f 100644
--- a/spec/controllers/admin/runners_controller_spec.rb
+++ b/spec/controllers/admin/runners_controller_spec.rb
@@ -12,9 +12,11 @@ RSpec.describe Admin::RunnersController do
describe '#index' do
render_views
- it 'lists all runners' do
+ before do
get :index
+ end
+ it 'renders index template' do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index)
end
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index e9a49319f21..e623c1ab940 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -501,11 +501,16 @@ RSpec.describe ApplicationController do
describe '#append_info_to_payload' do
controller(described_class) do
attr_reader :last_payload
+ urgency :high, [:foo]
def index
render html: 'authenticated'
end
+ def foo
+ render html: ''
+ end
+
def append_info_to_payload(payload)
super
@@ -513,6 +518,13 @@ RSpec.describe ApplicationController do
end
end
+ before do
+ routes.draw do
+ get 'index' => 'anonymous#index'
+ get 'foo' => 'anonymous#foo'
+ end
+ end
+
it 'does not log errors with a 200 response' do
get :index
@@ -534,6 +546,22 @@ RSpec.describe ApplicationController do
expect(controller.last_payload[:metadata]).to include('meta.user' => user.username)
end
+
+ context 'urgency information' do
+ it 'adds default urgency information to the payload' do
+ get :index
+
+ expect(controller.last_payload[:request_urgency]).to eq(:default)
+ expect(controller.last_payload[:target_duration_s]).to eq(1)
+ end
+
+ it 'adds customized urgency information to the payload' do
+ get :foo
+
+ expect(controller.last_payload[:request_urgency]).to eq(:high)
+ expect(controller.last_payload[:target_duration_s]).to eq(0.25)
+ end
+ end
end
describe '#access_denied' do
@@ -895,7 +923,7 @@ RSpec.describe ApplicationController do
describe '#set_current_context' do
controller(described_class) do
- feature_category :issue_tracking
+ feature_category :team_planning
def index
Gitlab::ApplicationContext.with_raw_context do |context|
@@ -949,7 +977,7 @@ RSpec.describe ApplicationController do
it 'sets the feature_category as defined in the controller' do
get :index, format: :json
- expect(json_response['meta.feature_category']).to eq('issue_tracking')
+ expect(json_response['meta.feature_category']).to eq('team_planning')
end
it 'assigns the context to a variable for logging' do
diff --git a/spec/controllers/concerns/group_tree_spec.rb b/spec/controllers/concerns/group_tree_spec.rb
index e808f1caa6e..921706b2042 100644
--- a/spec/controllers/concerns/group_tree_spec.rb
+++ b/spec/controllers/concerns/group_tree_spec.rb
@@ -102,13 +102,5 @@ RSpec.describe GroupTree do
end
it_behaves_like 'returns filtered groups'
-
- context 'when feature flag :linear_group_tree_ancestor_scopes is disabled' do
- before do
- stub_feature_flags(linear_group_tree_ancestor_scopes: false)
- end
-
- it_behaves_like 'returns filtered groups'
- end
end
end
diff --git a/spec/controllers/concerns/import_url_params_spec.rb b/spec/controllers/concerns/import_url_params_spec.rb
index 72f13cdcc94..ddffb243f7a 100644
--- a/spec/controllers/concerns/import_url_params_spec.rb
+++ b/spec/controllers/concerns/import_url_params_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe ImportUrlParams do
let(:import_url_params) do
- controller = OpenStruct.new(params: params).extend(described_class)
+ controller = double('controller', params: params).extend(described_class)
controller.import_url_params
end
diff --git a/spec/controllers/concerns/renders_commits_spec.rb b/spec/controllers/concerns/renders_commits_spec.rb
index 5c918267f50..acdeb98bb16 100644
--- a/spec/controllers/concerns/renders_commits_spec.rb
+++ b/spec/controllers/concerns/renders_commits_spec.rb
@@ -64,6 +64,12 @@ RSpec.describe RendersCommits do
subject.prepare_commits_for_rendering(merge_request.commits.take(1))
end
+ # Populate Banzai::Filter::References::ReferenceCache
+ subject.prepare_commits_for_rendering(merge_request.commits)
+
+ # Reset lazy_latest_pipeline cache to simulate a new request
+ BatchLoader::Executor.clear_current
+
expect do
subject.prepare_commits_for_rendering(merge_request.commits)
merge_request.commits.each(&:latest_pipeline)
diff --git a/spec/controllers/confirmations_controller_spec.rb b/spec/controllers/confirmations_controller_spec.rb
index 401ee36b387..1c7f8de32bb 100644
--- a/spec/controllers/confirmations_controller_spec.rb
+++ b/spec/controllers/confirmations_controller_spec.rb
@@ -123,4 +123,45 @@ RSpec.describe ConfirmationsController do
end
end
end
+
+ describe '#create' do
+ let(:user) { create(:user) }
+
+ subject(:perform_request) { post(:create, params: { user: { email: user.email } }) }
+
+ context 'when reCAPTCHA is disabled' do
+ before do
+ stub_application_setting(recaptcha_enabled: false)
+ end
+
+ it 'successfully sends password reset when reCAPTCHA is not solved' do
+ perform_request
+
+ expect(response).to redirect_to(dashboard_projects_path)
+ end
+ end
+
+ context 'when reCAPTCHA is enabled' do
+ before do
+ stub_application_setting(recaptcha_enabled: true)
+ end
+
+ it 'displays an error when the reCAPTCHA is not solved' do
+ Recaptcha.configuration.skip_verify_env.delete('test')
+
+ perform_request
+
+ expect(response).to render_template(:new)
+ expect(flash[:alert]).to include 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
+ end
+
+ it 'successfully sends password reset when reCAPTCHA is solved' do
+ Recaptcha.configuration.skip_verify_env << 'test'
+
+ perform_request
+
+ expect(response).to redirect_to(dashboard_projects_path)
+ end
+ end
+ end
end
diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb
index f0aa351bee0..cf528b414c0 100644
--- a/spec/controllers/dashboard/todos_controller_spec.rb
+++ b/spec/controllers/dashboard/todos_controller_spec.rb
@@ -62,7 +62,7 @@ RSpec.describe Dashboard::TodosController do
create(:issue, project: project, assignees: [user])
group_2 = create(:group)
group_2.add_owner(user)
- project_2 = create(:project)
+ project_2 = create(:project, namespace: user.namespace)
project_2.add_developer(user)
merge_request_2 = create(:merge_request, source_project: project_2)
create(:todo, project: project, author: author, user: user, target: merge_request_2)
diff --git a/spec/controllers/explore/projects_controller_spec.rb b/spec/controllers/explore/projects_controller_spec.rb
index 2297198878d..f2328303102 100644
--- a/spec/controllers/explore/projects_controller_spec.rb
+++ b/spec/controllers/explore/projects_controller_spec.rb
@@ -74,6 +74,28 @@ RSpec.describe Explore::ProjectsController do
end
end
end
+
+ describe 'GET #topic' do
+ context 'when topic does not exist' do
+ it 'renders a 404 error' do
+ get :topic, params: { topic_name: 'topic1' }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ context 'when topic exists' do
+ before do
+ create(:topic, name: 'topic1')
+ end
+
+ it 'renders the template' do
+ get :topic, params: { topic_name: 'topic1' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template('topic')
+ end
+ end
+ end
end
shared_examples "blocks high page numbers" do
diff --git a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb
index fa402d556c7..b22307578ab 100644
--- a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb
+++ b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb
@@ -124,6 +124,34 @@ RSpec.describe Groups::DependencyProxyForContainersController do
end
end
+ shared_examples 'authorize action with permission' do
+ context 'with a valid user' do
+ before do
+ group.add_guest(user)
+ end
+
+ it 'sends Workhorse local file instructions', :aggregate_failures do
+ subject
+
+ expect(response.headers['Content-Type']).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ expect(json_response['TempPath']).to eq(DependencyProxy::FileUploader.workhorse_local_upload_path)
+ expect(json_response['RemoteObject']).to be_nil
+ expect(json_response['MaximumSize']).to eq(maximum_size)
+ end
+
+ it 'sends Workhorse remote object instructions', :aggregate_failures do
+ stub_dependency_proxy_object_storage(direct_upload: true)
+
+ subject
+
+ expect(response.headers['Content-Type']).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ expect(json_response['TempPath']).to be_nil
+ expect(json_response['RemoteObject']).not_to be_nil
+ expect(json_response['MaximumSize']).to eq(maximum_size)
+ end
+ end
+ end
+
before do
allow(Gitlab.config.dependency_proxy)
.to receive(:enabled).and_return(true)
@@ -136,7 +164,8 @@ RSpec.describe Groups::DependencyProxyForContainersController do
end
describe 'GET #manifest' do
- let_it_be(:manifest) { create(:dependency_proxy_manifest) }
+ let_it_be(:tag) { 'latest' }
+ let_it_be(:manifest) { create(:dependency_proxy_manifest, file_name: "alpine:#{tag}.json", group: group) }
let(:pull_response) { { status: :success, manifest: manifest, from_cache: false } }
@@ -146,7 +175,7 @@ RSpec.describe Groups::DependencyProxyForContainersController do
end
end
- subject { get_manifest }
+ subject { get_manifest(tag) }
context 'feature enabled' do
before do
@@ -207,11 +236,26 @@ RSpec.describe Groups::DependencyProxyForContainersController do
it_behaves_like 'a successful manifest pull'
it_behaves_like 'a package tracking event', described_class.name, 'pull_manifest'
- context 'with a cache entry' do
- let(:pull_response) { { status: :success, manifest: manifest, from_cache: true } }
+ context 'with workhorse response' do
+ let(:pull_response) { { status: :success, manifest: nil, from_cache: false } }
- it_behaves_like 'returning response status', :success
- it_behaves_like 'a package tracking event', described_class.name, 'pull_manifest_from_cache'
+ it 'returns Workhorse send-dependency instructions', :aggregate_failures do
+ subject
+
+ send_data_type, send_data = workhorse_send_data
+ header, url = send_data.values_at('Header', 'Url')
+
+ expect(send_data_type).to eq('send-dependency')
+ expect(header).to eq(
+ "Authorization" => ["Bearer abcd1234"],
+ "Accept" => ::ContainerRegistry::Client::ACCEPTED_TYPES
+ )
+ expect(url).to eq(DependencyProxy::Registry.manifest_url('alpine', tag))
+ expect(response.headers['Content-Type']).to eq('application/gzip')
+ expect(response.headers['Content-Disposition']).to eq(
+ ActionDispatch::Http::ContentDisposition.format(disposition: 'attachment', filename: manifest.file_name)
+ )
+ end
end
end
@@ -237,8 +281,8 @@ RSpec.describe Groups::DependencyProxyForContainersController do
it_behaves_like 'not found when disabled'
- def get_manifest
- get :manifest, params: { group_id: group.to_param, image: 'alpine', tag: '3.9.2' }
+ def get_manifest(tag)
+ get :manifest, params: { group_id: group.to_param, image: 'alpine', tag: tag }
end
end
@@ -381,39 +425,28 @@ RSpec.describe Groups::DependencyProxyForContainersController do
end
end
- describe 'GET #authorize_upload_blob' do
+ describe 'POST #authorize_upload_blob' do
let(:blob_sha) { 'a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4' }
+ let(:maximum_size) { DependencyProxy::Blob::MAX_FILE_SIZE }
- subject(:authorize_upload_blob) do
+ subject do
request.headers.merge!(workhorse_internal_api_request_header)
- get :authorize_upload_blob, params: { group_id: group.to_param, image: 'alpine', sha: blob_sha }
+ post :authorize_upload_blob, params: { group_id: group.to_param, image: 'alpine', sha: blob_sha }
end
it_behaves_like 'without permission'
-
- context 'with a valid user' do
- before do
- group.add_guest(user)
- end
-
- it 'sends Workhorse file upload instructions', :aggregate_failures do
- authorize_upload_blob
-
- expect(response.headers['Content-Type']).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
- expect(json_response['TempPath']).to eq(DependencyProxy::FileUploader.workhorse_local_upload_path)
- end
- end
+ it_behaves_like 'authorize action with permission'
end
- describe 'GET #upload_blob' do
+ describe 'POST #upload_blob' do
let(:blob_sha) { 'a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4' }
let(:file) { fixture_file_upload("spec/fixtures/dependency_proxy/#{blob_sha}.gz", 'application/gzip') }
subject do
request.headers.merge!(workhorse_internal_api_request_header)
- get :upload_blob, params: {
+ post :upload_blob, params: {
group_id: group.to_param,
image: 'alpine',
sha: blob_sha,
@@ -436,6 +469,79 @@ RSpec.describe Groups::DependencyProxyForContainersController do
end
end
+ describe 'POST #authorize_upload_manifest' do
+ let(:maximum_size) { DependencyProxy::Manifest::MAX_FILE_SIZE }
+
+ subject do
+ request.headers.merge!(workhorse_internal_api_request_header)
+
+ post :authorize_upload_manifest, params: { group_id: group.to_param, image: 'alpine', tag: 'latest' }
+ end
+
+ it_behaves_like 'without permission'
+ it_behaves_like 'authorize action with permission'
+ end
+
+ describe 'POST #upload_manifest' do
+ let_it_be(:file) { fixture_file_upload("spec/fixtures/dependency_proxy/manifest", 'application/json') }
+ let_it_be(:image) { 'alpine' }
+ let_it_be(:tag) { 'latest' }
+ let_it_be(:content_type) { 'v2/manifest' }
+ let_it_be(:digest) { 'foo' }
+ let_it_be(:file_name) { "#{image}:#{tag}.json" }
+
+ subject do
+ request.headers.merge!(
+ workhorse_internal_api_request_header.merge!(
+ {
+ Gitlab::Workhorse::SEND_DEPENDENCY_CONTENT_TYPE_HEADER => content_type,
+ DependencyProxy::Manifest::DIGEST_HEADER => digest
+ }
+ )
+ )
+ params = {
+ group_id: group.to_param,
+ image: image,
+ tag: tag,
+ file: file,
+ file_name: file_name
+ }
+
+ post :upload_manifest, params: params
+ end
+
+ it_behaves_like 'without permission'
+
+ context 'with a valid user' do
+ before do
+ group.add_guest(user)
+ end
+
+ it_behaves_like 'a package tracking event', described_class.name, 'pull_manifest'
+
+ context 'with no existing manifest' do
+ it 'creates a manifest' do
+ expect { subject }.to change { group.dependency_proxy_manifests.count }.by(1)
+
+ manifest = group.dependency_proxy_manifests.first.reload
+ expect(manifest.content_type).to eq(content_type)
+ expect(manifest.digest).to eq(digest)
+ expect(manifest.file_name).to eq(file_name)
+ end
+ end
+
+ context 'with existing stale manifest' do
+ let_it_be(:old_digest) { 'asdf' }
+ let_it_be_with_reload(:manifest) { create(:dependency_proxy_manifest, file_name: file_name, digest: old_digest, group: group) }
+
+ it 'updates the existing manifest' do
+ expect { subject }.to change { group.dependency_proxy_manifests.count }.by(0)
+ .and change { manifest.reload.digest }.from(old_digest).to(digest)
+ end
+ end
+ end
+ end
+
def enable_dependency_proxy
group.create_dependency_proxy_setting!(enabled: true)
end
diff --git a/spec/controllers/groups/settings/integrations_controller_spec.rb b/spec/controllers/groups/settings/integrations_controller_spec.rb
index 31d1946652d..c070094babd 100644
--- a/spec/controllers/groups/settings/integrations_controller_spec.rb
+++ b/spec/controllers/groups/settings/integrations_controller_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Groups::Settings::IntegrationsController do
sign_in(user)
end
- it_behaves_like IntegrationsActions do
+ it_behaves_like Integrations::Actions do
let(:integration_attributes) { { group: group, project: nil } }
let(:routing_params) do
@@ -78,7 +78,7 @@ RSpec.describe Groups::Settings::IntegrationsController do
describe '#update' do
include JiraServiceHelper
- let(:integration) { create(:jira_integration, project: nil, group_id: group.id) }
+ let(:integration) { create(:jira_integration, :group, group: group) }
before do
group.add_owner(user)
@@ -108,7 +108,7 @@ RSpec.describe Groups::Settings::IntegrationsController do
end
describe '#reset' do
- let_it_be(:integration) { create(:jira_integration, group: group, project: nil) }
+ let_it_be(:integration) { create(:jira_integration, :group, group: group) }
let_it_be(:inheriting_integration) { create(:jira_integration, inherit_from_id: integration.id) }
subject do
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index a7625e65603..2525146c673 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -82,6 +82,16 @@ RSpec.describe GroupsController, factory_default: :keep do
expect(subject).to redirect_to group_import_path(group)
end
end
+
+ context 'publishing the invite_members_for_task experiment' do
+ it 'publishes the experiment data to the client' do
+ wrapped_experiment(experiment(:invite_members_for_task)) do |e|
+ expect(e).to receive(:publish_to_client)
+ end
+
+ get :show, params: { id: group.to_param, open_modal: 'invite_members_for_task' }, format: format
+ end
+ end
end
describe 'GET #details' do
diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb
index 0427715d1ac..91e43adc472 100644
--- a/spec/controllers/import/bitbucket_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_controller_spec.rb
@@ -252,6 +252,30 @@ RSpec.describe Import::BitbucketController do
end
end
end
+
+ context "when exceptions occur" do
+ shared_examples "handles exceptions" do
+ it "logs an exception" do
+ expect(Bitbucket::Client).to receive(:new).and_raise(error)
+ expect(controller).to receive(:log_exception)
+
+ post :create, format: :json
+ end
+ end
+
+ context "for OAuth2 errors" do
+ let(:fake_response) { double('Faraday::Response', headers: {}, body: '', status: 403) }
+ let(:error) { OAuth2::Error.new(OAuth2::Response.new(fake_response)) }
+
+ it_behaves_like "handles exceptions"
+ end
+
+ context "for Bitbucket errors" do
+ let(:error) { Bitbucket::Error::Unauthorized.new("error") }
+
+ it_behaves_like "handles exceptions"
+ end
+ end
end
context 'user has chosen an existing nested namespace and name for the project' do
diff --git a/spec/controllers/jira_connect/app_descriptor_controller_spec.rb b/spec/controllers/jira_connect/app_descriptor_controller_spec.rb
index 9d890efdd33..4f8b2b90637 100644
--- a/spec/controllers/jira_connect/app_descriptor_controller_spec.rb
+++ b/spec/controllers/jira_connect/app_descriptor_controller_spec.rb
@@ -90,17 +90,5 @@ RSpec.describe JiraConnect::AppDescriptorController do
)
)
end
-
- context 'when jira_connect_asymmetric_jwt is disabled' do
- before do
- stub_feature_flags(jira_connect_asymmetric_jwt: false)
- end
-
- specify do
- get :show
-
- expect(json_response).to include('apiMigrations' => include('signed-install' => false))
- end
- end
end
end
diff --git a/spec/controllers/jira_connect/events_controller_spec.rb b/spec/controllers/jira_connect/events_controller_spec.rb
index 78bd0dc8318..2a70a2ea683 100644
--- a/spec/controllers/jira_connect/events_controller_spec.rb
+++ b/spec/controllers/jira_connect/events_controller_spec.rb
@@ -77,18 +77,6 @@ RSpec.describe JiraConnect::EventsController do
expect(installation.base_url).to eq('https://test.atlassian.net')
end
- context 'when jira_connect_asymmetric_jwt is disabled' do
- before do
- stub_feature_flags(jira_connect_asymmetric_jwt: false)
- end
-
- it 'saves the jira installation data without JWT validation' do
- expect(Atlassian::JiraConnect::AsymmetricJwt).not_to receive(:new)
-
- expect { subject }.to change { JiraConnectInstallation.count }.by(1)
- end
- end
-
context 'when it is a version update and shared_secret is not sent' do
let(:params) do
{
@@ -110,22 +98,6 @@ RSpec.describe JiraConnect::EventsController do
expect { subject }.not_to change { JiraConnectInstallation.count }
expect(response).to have_gitlab_http_status(:ok)
end
-
- context 'when jira_connect_asymmetric_jwt is disabled' do
- before do
- stub_feature_flags(jira_connect_asymmetric_jwt: false)
- end
-
- it 'decodes the JWT token in authorization header and returns 200 without creating a new installation' do
- request.headers["Authorization"] = "Bearer #{Atlassian::Jwt.encode({ iss: client_key }, shared_secret)}"
-
- expect(Atlassian::JiraConnect::AsymmetricJwt).not_to receive(:new)
-
- expect { subject }.not_to change { JiraConnectInstallation.count }
-
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
end
end
end
@@ -153,23 +125,6 @@ RSpec.describe JiraConnect::EventsController do
it 'does not delete the installation' do
expect { post_uninstalled }.not_to change { JiraConnectInstallation.count }
end
-
- context 'when jira_connect_asymmetric_jwt is disabled' do
- before do
- stub_feature_flags(jira_connect_asymmetric_jwt: false)
- request.headers['Authorization'] = 'JWT invalid token'
- end
-
- it 'returns 403' do
- post_uninstalled
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
-
- it 'does not delete the installation' do
- expect { post_uninstalled }.not_to change { JiraConnectInstallation.count }
- end
- end
end
context 'when JWT is valid' do
@@ -197,36 +152,6 @@ RSpec.describe JiraConnect::EventsController do
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
-
- context 'when jira_connect_asymmetric_jwt is disabled' do
- before do
- stub_feature_flags(jira_connect_asymmetric_jwt: false)
-
- request.headers['Authorization'] = "JWT #{Atlassian::Jwt.encode({ iss: installation.client_key, qsh: qsh }, installation.shared_secret)}"
- end
-
- let(:qsh) { Atlassian::Jwt.create_query_string_hash('https://gitlab.test/events/uninstalled', 'POST', 'https://gitlab.test') }
-
- it 'calls the DestroyService and returns ok in case of success' do
- expect_next_instance_of(JiraConnectInstallations::DestroyService, installation, jira_base_path, jira_event_path) do |destroy_service|
- expect(destroy_service).to receive(:execute).and_return(true)
- end
-
- post_uninstalled
-
- expect(response).to have_gitlab_http_status(:ok)
- end
-
- it 'calls the DestroyService and returns unprocessable_entity in case of failure' do
- expect_next_instance_of(JiraConnectInstallations::DestroyService, installation, jira_base_path, jira_event_path) do |destroy_service|
- expect(destroy_service).to receive(:execute).and_return(false)
- end
-
- post_uninstalled
-
- expect(response).to have_gitlab_http_status(:unprocessable_entity)
- end
- end
end
end
end
diff --git a/spec/controllers/oauth/authorizations_controller_spec.rb b/spec/controllers/oauth/authorizations_controller_spec.rb
index 0e25f6a96d7..98cc8d83e0c 100644
--- a/spec/controllers/oauth/authorizations_controller_spec.rb
+++ b/spec/controllers/oauth/authorizations_controller_spec.rb
@@ -3,8 +3,7 @@
require 'spec_helper'
RSpec.describe Oauth::AuthorizationsController do
- let(:user) { create(:user, confirmed_at: confirmed_at) }
- let(:confirmed_at) { 1.hour.ago }
+ let(:user) { create(:user) }
let!(:application) { create(:oauth_application, scopes: 'api read_user', redirect_uri: 'http://example.com') }
let(:params) do
{
@@ -40,7 +39,7 @@ RSpec.describe Oauth::AuthorizationsController do
end
context 'when the user is unconfirmed' do
- let(:confirmed_at) { nil }
+ let(:user) { create(:user, :unconfirmed) }
it 'returns 200 and renders error view' do
subject
@@ -73,8 +72,6 @@ RSpec.describe Oauth::AuthorizationsController do
include_examples "Implicit grant can't be used in confidential application"
context 'when the user is confirmed' do
- let(:confirmed_at) { 1.hour.ago }
-
context 'when there is already an access token for the application with a matching scope' do
before do
scopes = Doorkeeper::OAuth::Scopes.from_string('api')
diff --git a/spec/controllers/passwords_controller_spec.rb b/spec/controllers/passwords_controller_spec.rb
index 08d68d7cec8..01c032d9e3b 100644
--- a/spec/controllers/passwords_controller_spec.rb
+++ b/spec/controllers/passwords_controller_spec.rb
@@ -91,4 +91,47 @@ RSpec.describe PasswordsController do
end
end
end
+
+ describe '#create' do
+ let(:user) { create(:user) }
+
+ subject(:perform_request) { post(:create, params: { user: { email: user.email } }) }
+
+ context 'when reCAPTCHA is disabled' do
+ before do
+ stub_application_setting(recaptcha_enabled: false)
+ end
+
+ it 'successfully sends password reset when reCAPTCHA is not solved' do
+ perform_request
+
+ expect(response).to redirect_to(new_user_session_path)
+ expect(flash[:notice]).to include 'If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes.'
+ end
+ end
+
+ context 'when reCAPTCHA is enabled' do
+ before do
+ stub_application_setting(recaptcha_enabled: true)
+ end
+
+ it 'displays an error when the reCAPTCHA is not solved' do
+ Recaptcha.configuration.skip_verify_env.delete('test')
+
+ perform_request
+
+ expect(response).to render_template(:new)
+ expect(flash[:alert]).to include 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
+ end
+
+ it 'successfully sends password reset when reCAPTCHA is solved' do
+ Recaptcha.configuration.skip_verify_env << 'test'
+
+ perform_request
+
+ expect(response).to redirect_to(new_user_session_path)
+ expect(flash[:notice]).to include 'If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes.'
+ end
+ end
+ end
end
diff --git a/spec/controllers/profiles/accounts_controller_spec.rb b/spec/controllers/profiles/accounts_controller_spec.rb
index c6e7866a659..011528016ce 100644
--- a/spec/controllers/profiles/accounts_controller_spec.rb
+++ b/spec/controllers/profiles/accounts_controller_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe Profiles::AccountsController do
end
end
- [:twitter, :facebook, :google_oauth2, :gitlab, :github, :bitbucket, :crowd, :auth0, :authentiq].each do |provider|
+ [:twitter, :facebook, :google_oauth2, :gitlab, :github, :bitbucket, :crowd, :auth0, :authentiq, :dingtalk].each do |provider|
describe "#{provider} provider" do
let(:user) { create(:omniauth_user, provider: provider.to_s) }
diff --git a/spec/controllers/profiles/two_factor_auths_controller_spec.rb b/spec/controllers/profiles/two_factor_auths_controller_spec.rb
index e57bd5be937..47086ccdd2c 100644
--- a/spec/controllers/profiles/two_factor_auths_controller_spec.rb
+++ b/spec/controllers/profiles/two_factor_auths_controller_spec.rb
@@ -62,6 +62,32 @@ RSpec.describe Profiles::TwoFactorAuthsController do
expect(flash[:alert]).to be_nil
end
end
+
+ context 'when password authentication is disabled' do
+ before do
+ stub_application_setting(password_authentication_enabled_for_web: false)
+ end
+
+ it 'does not require the current password', :aggregate_failures do
+ go
+
+ expect(response).not_to redirect_to(redirect_path)
+ expect(flash[:alert]).to be_nil
+ end
+ end
+
+ context 'when the user is an LDAP user' do
+ before do
+ allow(user).to receive(:ldap_user?).and_return(true)
+ end
+
+ it 'does not require the current password', :aggregate_failures do
+ go
+
+ expect(response).not_to redirect_to(redirect_path)
+ expect(flash[:alert]).to be_nil
+ end
+ end
end
describe 'GET show' do
@@ -149,7 +175,7 @@ RSpec.describe Profiles::TwoFactorAuthsController do
it 'assigns error' do
go
- expect(assigns[:error]).to eq _('Invalid pin code')
+ expect(assigns[:error]).to eq({ message: 'Invalid pin code.' })
end
it 'assigns qr_code' do
diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb
index 4959003d788..9a1f8a8442d 100644
--- a/spec/controllers/profiles_controller_spec.rb
+++ b/spec/controllers/profiles_controller_spec.rb
@@ -125,6 +125,8 @@ RSpec.describe ProfilesController, :request_store do
end
describe 'GET audit_log' do
+ let(:auth_event) { create(:authentication_event, user: user) }
+
it 'tracks search event', :snowplow do
sign_in(user)
@@ -136,6 +138,14 @@ RSpec.describe ProfilesController, :request_store do
user: user
)
end
+
+ it 'loads page correctly' do
+ sign_in(user)
+
+ get :audit_log
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
end
describe 'PUT update_username' do
diff --git a/spec/controllers/projects/alerting/notifications_controller_spec.rb b/spec/controllers/projects/alerting/notifications_controller_spec.rb
index 2fff8026b22..b3feeb7c07b 100644
--- a/spec/controllers/projects/alerting/notifications_controller_spec.rb
+++ b/spec/controllers/projects/alerting/notifications_controller_spec.rb
@@ -16,7 +16,9 @@ RSpec.describe Projects::Alerting::NotificationsController do
end
shared_examples 'process alert payload' do |notify_service_class|
- let(:service_response) { ServiceResponse.success }
+ let(:alert_1) { build(:alert_management_alert, project: project) }
+ let(:alert_2) { build(:alert_management_alert, project: project) }
+ let(:service_response) { ServiceResponse.success(payload: { alerts: [alert_1, alert_2] }) }
let(:notify_service) { instance_double(notify_service_class, execute: service_response) }
before do
@@ -30,9 +32,13 @@ RSpec.describe Projects::Alerting::NotificationsController do
context 'when notification service succeeds' do
let(:permitted_params) { ActionController::Parameters.new(payload).permit! }
- it 'responds with ok' do
+ it 'responds with the alert data' do
make_request
+ expect(json_response).to contain_exactly(
+ { 'iid' => alert_1.iid, 'title' => alert_1.title },
+ { 'iid' => alert_2.iid, 'title' => alert_2.title }
+ )
expect(response).to have_gitlab_http_status(:ok)
end
diff --git a/spec/controllers/projects/analytics/cycle_analytics/stages_controller_spec.rb b/spec/controllers/projects/analytics/cycle_analytics/stages_controller_spec.rb
index 1351ba35a71..3f0318c3973 100644
--- a/spec/controllers/projects/analytics/cycle_analytics/stages_controller_spec.rb
+++ b/spec/controllers/projects/analytics/cycle_analytics/stages_controller_spec.rb
@@ -16,6 +16,7 @@ RSpec.describe Projects::Analytics::CycleAnalytics::StagesController do
end
before do
+ stub_feature_flags(use_vsa_aggregated_tables: false)
sign_in(user)
end
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index 43e8bbd83cf..d9dedb04b0d 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -356,7 +356,7 @@ RSpec.describe Projects::BranchesController do
context "valid branch name with encoded slashes" do
let(:branch) { "improve%2Fawesome" }
- it { expect(response).to have_gitlab_http_status(:ok) }
+ it { expect(response).to have_gitlab_http_status(:not_found) }
it { expect(response.body).to be_blank }
end
@@ -396,10 +396,10 @@ RSpec.describe Projects::BranchesController do
let(:branch) { 'improve%2Fawesome' }
it 'returns JSON response with message' do
- expect(json_response).to eql('message' => 'Branch was deleted')
+ expect(json_response).to eql('message' => 'No such branch')
end
- it { expect(response).to have_gitlab_http_status(:ok) }
+ it { expect(response).to have_gitlab_http_status(:not_found) }
end
context 'invalid branch name, valid ref' do
diff --git a/spec/controllers/projects/ci/pipeline_editor_controller_spec.rb b/spec/controllers/projects/ci/pipeline_editor_controller_spec.rb
index 942402a6d00..d55aad20689 100644
--- a/spec/controllers/projects/ci/pipeline_editor_controller_spec.rb
+++ b/spec/controllers/projects/ci/pipeline_editor_controller_spec.rb
@@ -6,6 +6,8 @@ RSpec.describe Projects::Ci::PipelineEditorController do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
+ subject(:show_request) { get :show, params: { namespace_id: project.namespace, project_id: project } }
+
before do
sign_in(user)
end
@@ -14,8 +16,7 @@ RSpec.describe Projects::Ci::PipelineEditorController do
context 'with enough privileges' do
before do
project.add_developer(user)
-
- get :show, params: { namespace_id: project.namespace, project_id: project }
+ show_request
end
it { expect(response).to have_gitlab_http_status(:ok) }
@@ -28,13 +29,24 @@ RSpec.describe Projects::Ci::PipelineEditorController do
context 'without enough privileges' do
before do
project.add_reporter(user)
-
- get :show, params: { namespace_id: project.namespace, project_id: project }
+ show_request
end
it 'responds with 404' do
expect(response).to have_gitlab_http_status(:not_found)
end
end
+
+ describe 'pipeline_editor_walkthrough experiment' do
+ before do
+ project.add_developer(user)
+ end
+
+ subject(:action) { show_request }
+
+ it_behaves_like 'tracks assignment and records the subject', :pipeline_editor_walkthrough, :namespace do
+ subject { project.namespace }
+ end
+ end
end
end
diff --git a/spec/controllers/projects/commits_controller_spec.rb b/spec/controllers/projects/commits_controller_spec.rb
index 4cf77fde3a1..a8e71d73beb 100644
--- a/spec/controllers/projects/commits_controller_spec.rb
+++ b/spec/controllers/projects/commits_controller_spec.rb
@@ -67,6 +67,29 @@ RSpec.describe Projects::CommitsController do
end
end
+ context "with an invalid limit" do
+ let(:id) { "master/README.md" }
+
+ it "uses the default limit" do
+ expect_any_instance_of(Repository).to receive(:commits).with(
+ "master",
+ path: "README.md",
+ limit: described_class::COMMITS_DEFAULT_LIMIT,
+ offset: 0
+ ).and_call_original
+
+ get(:show,
+ params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: id,
+ limit: "foo"
+ })
+
+ expect(response).to be_successful
+ end
+ end
+
context "when the ref name ends in .atom" do
context "when the ref does not exist with the suffix" do
before do
diff --git a/spec/controllers/projects/hooks_controller_spec.rb b/spec/controllers/projects/hooks_controller_spec.rb
index 17baf38ef32..2ab18ccddbf 100644
--- a/spec/controllers/projects/hooks_controller_spec.rb
+++ b/spec/controllers/projects/hooks_controller_spec.rb
@@ -109,7 +109,7 @@ RSpec.describe Projects::HooksController do
describe '#test' do
let(:hook) { create(:project_hook, project: project) }
- context 'when the endpoint receives requests above the limit' do
+ context 'when the endpoint receives requests above the limit', :freeze_time, :clean_gitlab_redis_rate_limiting do
before do
allow(Gitlab::ApplicationRateLimiter).to receive(:rate_limits)
.and_return(project_testing_hook: { threshold: 1, interval: 1.minute })
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 0b3bd4d78ac..68cccfa8bde 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -1084,28 +1084,30 @@ RSpec.describe Projects::IssuesController do
end
context 'real-time sidebar feature flag' do
- using RSpec::Parameterized::TableSyntax
-
let_it_be(:project) { create(:project, :public) }
let_it_be(:issue) { create(:issue, project: project) }
- where(:action_cable_in_app_enabled, :feature_flag_enabled, :gon_feature_flag) do
- true | true | true
- true | false | true
- false | true | true
- false | false | false
+ context 'when enabled' do
+ before do
+ stub_feature_flags(real_time_issue_sidebar: true)
+ end
+
+ it 'pushes the correct value to the frontend' do
+ go(id: issue.to_param)
+
+ expect(Gon.features).to include('realTimeIssueSidebar' => true)
+ end
end
- with_them do
+ context 'when disabled' do
before do
- expect(Gitlab::ActionCable::Config).to receive(:in_app?).and_return(action_cable_in_app_enabled)
- stub_feature_flags(real_time_issue_sidebar: feature_flag_enabled)
+ stub_feature_flags(real_time_issue_sidebar: false)
end
- it 'broadcasts to the issues channel based on ActionCable and feature flag values' do
+ it 'pushes the correct value to the frontend' do
go(id: issue.to_param)
- expect(Gon.features).to include('realTimeIssueSidebar' => gon_feature_flag)
+ expect(Gon.features).to include('realTimeIssueSidebar' => false)
end
end
end
@@ -1406,14 +1408,14 @@ RSpec.describe Projects::IssuesController do
end
end
- context 'when the endpoint receives requests above the limit' do
+ context 'when the endpoint receives requests above the limit', :freeze_time, :clean_gitlab_redis_rate_limiting do
before do
- stub_application_setting(issues_create_limit: 5)
+ stub_application_setting(issues_create_limit: 1)
end
context 'when issue creation limits imposed' do
it 'prevents from creating more issues', :request_store do
- 5.times { post_new_issue }
+ post_new_issue
expect { post_new_issue }
.to change { Gitlab::GitalyClient.get_request_count }.by(1) # creates 1 projects and 0 issues
@@ -1440,7 +1442,7 @@ RSpec.describe Projects::IssuesController do
project.add_developer(user)
sign_in(user)
- 6.times do
+ 2.times do
post :create, params: {
namespace_id: project.namespace.to_param,
project_id: project,
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index 06c29e767ad..ed68d6a87b8 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -463,12 +463,25 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
end
- context 'when job has trace' do
+ context 'when job has live trace' do
let(:job) { create(:ci_build, :running, :trace_live, pipeline: pipeline) }
- it "has_trace is true" do
+ it 'has_trace is true' do
get_show_json
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('job/job_details')
+ expect(json_response['has_trace']).to be true
+ end
+ end
+
+ context 'when has live trace and unarchived artifact' do
+ let(:job) { create(:ci_build, :running, :trace_live, :unarchived_trace_artifact, pipeline: pipeline) }
+
+ it 'has_trace is true' do
+ get_show_json
+
+ expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('job/job_details')
expect(json_response['has_trace']).to be true
end
@@ -631,15 +644,25 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
end
- context 'when job has a trace' do
+ context 'when job has a live trace' do
let(:job) { create(:ci_build, :trace_live, pipeline: pipeline) }
- it 'returns a trace' do
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('job/build_trace')
- expect(json_response['id']).to eq job.id
- expect(json_response['status']).to eq job.status
- expect(json_response['lines']).to eq [{ 'content' => [{ 'text' => 'BUILD TRACE' }], 'offset' => 0 }]
+ shared_examples_for 'returns trace' do
+ it 'returns a trace' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('job/build_trace')
+ expect(json_response['id']).to eq job.id
+ expect(json_response['status']).to eq job.status
+ expect(json_response['lines']).to match_array [{ 'content' => [{ 'text' => 'BUILD TRACE' }], 'offset' => 0 }]
+ end
+ end
+
+ it_behaves_like 'returns trace'
+
+ context 'when job has unarchived artifact' do
+ let(:job) { create(:ci_build, :trace_live, :unarchived_trace_artifact, pipeline: pipeline) }
+
+ it_behaves_like 'returns trace'
end
context 'when job is running' do
@@ -1055,9 +1078,7 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
post_erase
end
- context 'when job is erasable' do
- let(:job) { create(:ci_build, :erasable, :trace_artifact, pipeline: pipeline) }
-
+ shared_examples_for 'erases' do
it 'redirects to the erased job page' do
expect(response).to have_gitlab_http_status(:found)
expect(response).to redirect_to(namespace_project_job_path(id: job.id))
@@ -1073,7 +1094,19 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
end
- context 'when job is not erasable' do
+ context 'when job is successful and has artifacts' do
+ let(:job) { create(:ci_build, :erasable, :trace_artifact, pipeline: pipeline) }
+
+ it_behaves_like 'erases'
+ end
+
+ context 'when job has live trace and unarchived artifact' do
+ let(:job) { create(:ci_build, :success, :trace_live, :unarchived_trace_artifact, pipeline: pipeline) }
+
+ it_behaves_like 'erases'
+ end
+
+ context 'when job is erased' do
let(:job) { create(:ci_build, :erased, pipeline: pipeline) }
it 'returns unprocessable_entity' do
@@ -1165,16 +1198,26 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
end
- context "when job has a trace file" do
+ context 'when job has a live trace' do
let(:job) { create(:ci_build, :trace_live, pipeline: pipeline) }
- it 'sends a trace file' do
- response = subject
+ shared_examples_for 'sends live trace' do
+ it 'sends a trace file' do
+ response = subject
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8")
- expect(response.headers["Content-Disposition"]).to match(/^inline/)
- expect(response.body).to eq("BUILD TRACE")
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8")
+ expect(response.headers["Content-Disposition"]).to match(/^inline/)
+ expect(response.body).to eq("BUILD TRACE")
+ end
+ end
+
+ it_behaves_like 'sends live trace'
+
+ context 'and when job has unarchived artifact' do
+ let(:job) { create(:ci_build, :trace_live, :unarchived_trace_artifact, pipeline: pipeline) }
+
+ it_behaves_like 'sends live trace'
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 3d7636b1f30..5b1c6777523 100644
--- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
@@ -86,10 +86,11 @@ RSpec.describe Projects::MergeRequests::DiffsController do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
+ let(:maintainer) { true }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
before do
- project.add_maintainer(user)
+ project.add_maintainer(user) if maintainer
sign_in(user)
end
@@ -383,8 +384,9 @@ RSpec.describe Projects::MergeRequests::DiffsController do
end
context 'when the user cannot view the merge request' do
+ let(:maintainer) { false }
+
before do
- project.team.truncate
diff_for_path(old_path: existing_path, new_path: existing_path)
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 438fc2f2106..46b332a8938 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -10,7 +10,8 @@ RSpec.describe Projects::MergeRequestsController do
let_it_be_with_reload(:project_public_with_private_builds) { create(:project, :repository, :public, :builds_private) }
let(:user) { project.owner }
- let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
+ let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: merge_request_source_project, allow_collaboration: false) }
+ let(:merge_request_source_project) { project }
before do
sign_in(user)
@@ -506,6 +507,7 @@ RSpec.describe Projects::MergeRequestsController do
end
it 'starts the merge immediately with permitted params' do
+ allow(MergeWorker).to receive(:with_status).and_return(MergeWorker)
expect(MergeWorker).to receive(:perform_async).with(merge_request.id, anything, { 'sha' => merge_request.diff_head_sha })
merge_with_sha
@@ -2073,19 +2075,21 @@ RSpec.describe Projects::MergeRequestsController do
end
describe 'POST #rebase' do
- let(:viewer) { user }
-
def post_rebase
post :rebase, params: { namespace_id: project.namespace, project_id: project, id: merge_request }
end
+ before do
+ allow(RebaseWorker).to receive(:with_status).and_return(RebaseWorker)
+ end
+
def expect_rebase_worker_for(user)
expect(RebaseWorker).to receive(:perform_async).with(merge_request.id, user.id, false)
end
context 'successfully' do
it 'enqeues a RebaseWorker' do
- expect_rebase_worker_for(viewer)
+ expect_rebase_worker_for(user)
post_rebase
@@ -2108,17 +2112,17 @@ RSpec.describe Projects::MergeRequestsController do
context 'with a forked project' do
let(:forked_project) { fork_project(project, fork_owner, repository: true) }
let(:fork_owner) { create(:user) }
+ let(:merge_request_source_project) { forked_project }
- before do
- project.add_developer(fork_owner)
+ context 'user cannot push to source branch' do
+ before do
+ project.add_developer(fork_owner)
- merge_request.update!(source_project: forked_project)
- forked_project.add_reporter(user)
- end
+ forked_project.add_reporter(user)
+ end
- context 'user cannot push to source branch' do
it 'returns 404' do
- expect_rebase_worker_for(viewer).never
+ expect_rebase_worker_for(user).never
post_rebase
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index d92862f0ca3..66af546b113 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -1007,6 +1007,35 @@ RSpec.describe Projects::NotesController do
end
end
+ describe 'GET outdated_line_change' do
+ let(:request_params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: note,
+ format: 'json'
+ }
+ end
+
+ before do
+ service = double
+ allow(service).to receive(:execute).and_return([{ line_text: 'Test' }])
+ allow(MergeRequests::OutdatedDiscussionDiffLinesService).to receive(:new).once.and_return(service)
+
+ sign_in(user)
+ project.add_developer(user)
+ end
+
+ it "successfully renders expected JSON response" do
+ get :outdated_line_change, params: request_params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an(Array)
+ expect(json_response.count).to eq(1)
+ expect(json_response.first).to include({ "line_text" => "Test" })
+ end
+ end
+
# Convert a time to an integer number of microseconds
def microseconds(time)
(time.to_i * 1_000_000) + time.usec
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 1354e894872..14c613ff9c4 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -44,7 +44,7 @@ RSpec.describe Projects::PipelinesController do
end
end
- it 'does not execute N+1 queries' do
+ it 'does not execute N+1 queries', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/345470' do
get_pipelines_index_json
control_count = ActiveRecord::QueryRecorder.new do
diff --git a/spec/controllers/projects/prometheus/alerts_controller_spec.rb b/spec/controllers/projects/prometheus/alerts_controller_spec.rb
index 46de8aa4baf..d66ad445c32 100644
--- a/spec/controllers/projects/prometheus/alerts_controller_spec.rb
+++ b/spec/controllers/projects/prometheus/alerts_controller_spec.rb
@@ -160,7 +160,9 @@ RSpec.describe Projects::Prometheus::AlertsController do
end
describe 'POST #notify' do
- let(:service_response) { ServiceResponse.success }
+ let(:alert_1) { build(:alert_management_alert, :prometheus, project: project) }
+ let(:alert_2) { build(:alert_management_alert, :prometheus, project: project) }
+ let(:service_response) { ServiceResponse.success(payload: { alerts: [alert_1, alert_2] }) }
let(:notify_service) { instance_double(Projects::Prometheus::Alerts::NotifyService, execute: service_response) }
before do
@@ -173,10 +175,15 @@ RSpec.describe Projects::Prometheus::AlertsController do
end
it 'returns ok if notification succeeds' do
- expect(notify_service).to receive(:execute).and_return(ServiceResponse.success)
+ expect(notify_service).to receive(:execute).and_return(service_response)
post :notify, params: project_params, session: { as: :json }
+ expect(json_response).to contain_exactly(
+ { 'iid' => alert_1.iid, 'title' => alert_1.title },
+ { 'iid' => alert_2.iid, 'title' => alert_2.title }
+ )
+
expect(response).to have_gitlab_http_status(:ok)
end
diff --git a/spec/controllers/projects/releases_controller_spec.rb b/spec/controllers/projects/releases_controller_spec.rb
index a1e36ec5c4c..120020273f9 100644
--- a/spec/controllers/projects/releases_controller_spec.rb
+++ b/spec/controllers/projects/releases_controller_spec.rb
@@ -207,7 +207,18 @@ RSpec.describe Projects::ReleasesController do
let(:project) { private_project }
let(:user) { guest }
- it_behaves_like 'not found'
+ it_behaves_like 'successful request'
+ end
+
+ context 'when user is an external user for the project' do
+ let(:project) { private_project }
+ let(:user) { create(:user) }
+
+ it 'behaves like not found' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
end
end
diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb
index 482ba552f8f..29988da6e60 100644
--- a/spec/controllers/projects/services_controller_spec.rb
+++ b/spec/controllers/projects/services_controller_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe Projects::ServicesController do
project.add_maintainer(user)
end
- it_behaves_like IntegrationsActions do
+ it_behaves_like Integrations::Actions do
let(:integration_attributes) { { project: project } }
let(:routing_params) do
@@ -254,7 +254,7 @@ RSpec.describe Projects::ServicesController do
let_it_be(:project) { create(:project, group: group) }
let_it_be(:jira_integration) { create(:jira_integration, project: project) }
- let(:group_integration) { create(:jira_integration, group: group, project: nil, url: 'http://group.com', password: 'group') }
+ let(:group_integration) { create(:jira_integration, :group, group: group, url: 'http://group.com', password: 'group') }
let(:integration_params) { { inherit_from_id: group_integration.id, url: 'http://custom.com', password: 'custom' } }
it 'ignores submitted params and inherits group settings' do
@@ -269,7 +269,7 @@ RSpec.describe Projects::ServicesController do
context 'when param `inherit_from_id` is set to an unrelated group' do
let_it_be(:group) { create(:group) }
- let(:group_integration) { create(:jira_integration, group: group, project: nil, url: 'http://group.com', password: 'group') }
+ let(:group_integration) { create(:jira_integration, :group, group: group, url: 'http://group.com', password: 'group') }
let(:integration_params) { { inherit_from_id: group_integration.id, url: 'http://custom.com', password: 'custom' } }
it 'ignores the param and saves the submitted settings' do
diff --git a/spec/controllers/projects/tags_controller_spec.rb b/spec/controllers/projects/tags_controller_spec.rb
index d0719643b7f..0045c0a484b 100644
--- a/spec/controllers/projects/tags_controller_spec.rb
+++ b/spec/controllers/projects/tags_controller_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe Projects::TagsController do
with_them do
it 'returns 503 status code' do
expect_next_instance_of(TagsFinder) do |finder|
- expect(finder).to receive(:execute).and_return([[], Gitlab::Git::CommandError.new])
+ expect(finder).to receive(:execute).and_raise(Gitlab::Git::CommandError)
end
get :index, params: { namespace_id: project.namespace.to_param, project_id: project }, format: format
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index b34cfedb767..dafa639a2d5 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -213,21 +213,6 @@ RSpec.describe ProjectsController do
before do
sign_in(user)
-
- allow(controller).to receive(:record_experiment_user)
- end
-
- context 'when user can push to default branch', :experiment do
- let(:user) { empty_project.owner }
-
- it 'creates an "view_project_show" experiment tracking event' do
- expect(experiment(:empty_repo_upload)).to track(
- :view_project_show,
- property: 'empty'
- ).on_next_instance
-
- get :show, params: { namespace_id: empty_project.namespace, id: empty_project }
- end
end
User.project_views.keys.each do |project_view|
@@ -1158,6 +1143,22 @@ RSpec.describe ProjectsController do
expect(json_response["Commits"]).to include("123456")
end
+ context 'when gitaly is unavailable' do
+ before do
+ expect_next_instance_of(TagsFinder) do |finder|
+ allow(finder).to receive(:execute).and_raise(Gitlab::Git::CommandError)
+ end
+ end
+
+ it 'gets an empty list of tags' do
+ get :refs, params: { namespace_id: project.namespace, id: project, ref: "123456" }
+
+ expect(json_response["Branches"]).to include("master")
+ expect(json_response["Tags"]).to eq([])
+ expect(json_response["Commits"]).to include("123456")
+ end
+ end
+
context "when preferred language is Japanese" do
before do
user.update!(preferred_language: 'ja')
diff --git a/spec/controllers/registrations/welcome_controller_spec.rb b/spec/controllers/registrations/welcome_controller_spec.rb
index 034c9b3d1c0..0a1e6b8ec8f 100644
--- a/spec/controllers/registrations/welcome_controller_spec.rb
+++ b/spec/controllers/registrations/welcome_controller_spec.rb
@@ -97,6 +97,16 @@ RSpec.describe Registrations::WelcomeController do
expect(subject).to redirect_to(dashboard_projects_path)
end
end
+
+ context 'when tasks to be done are assigned' do
+ let!(:member1) { create(:group_member, user: user, tasks_to_be_done: %w(ci code)) }
+
+ before do
+ stub_experiments(invite_members_for_task: true)
+ end
+
+ it { is_expected.to redirect_to(issues_dashboard_path(assignee_username: user.username)) }
+ end
end
end
end
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index a25c597edb2..baf500c2b57 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -499,13 +499,12 @@ RSpec.describe RegistrationsController do
expect(User.last.name).to eq("#{base_user_params[:first_name]} #{base_user_params[:last_name]}")
end
- it 'sets the username and caller_id in the context' do
+ it 'sets the caller_id in the context' do
expect(controller).to receive(:create).and_wrap_original do |m, *args|
m.call(*args)
expect(Gitlab::ApplicationContext.current)
- .to include('meta.user' => base_user_params[:username],
- 'meta.caller_id' => 'RegistrationsController#create')
+ .to include('meta.caller_id' => 'RegistrationsController#create')
end
subject
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index 5eccb0b46ef..521b4cd4002 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -18,8 +18,8 @@ RSpec.describe 'Database schema' do
approvals: %w[user_id],
approver_groups: %w[target_id],
approvers: %w[target_id user_id],
- analytics_cycle_analytics_merge_request_stage_events: %w[author_id group_id merge_request_id milestone_id project_id stage_event_hash_id],
- analytics_cycle_analytics_issue_stage_events: %w[author_id group_id issue_id milestone_id project_id stage_event_hash_id],
+ analytics_cycle_analytics_merge_request_stage_events: %w[author_id group_id merge_request_id milestone_id project_id stage_event_hash_id state_id],
+ analytics_cycle_analytics_issue_stage_events: %w[author_id group_id issue_id milestone_id project_id stage_event_hash_id state_id],
audit_events: %w[author_id entity_id target_id],
award_emoji: %w[awardable_id user_id],
aws_roles: %w[role_external_id],
@@ -29,6 +29,7 @@ RSpec.describe 'Database schema' do
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_pipeline_chat_data: %w[chat_name_id], # it uses the loose foreign key featue
ci_runner_projects: %w[runner_id],
ci_trigger_requests: %w[commit_id],
cluster_providers_aws: %w[security_group_id vpc_id access_key_id],
@@ -48,7 +49,6 @@ RSpec.describe 'Database schema' do
geo_node_statuses: %w[last_event_id cursor_last_event_id],
geo_nodes: %w[oauth_application_id],
geo_repository_deleted_events: %w[project_id],
- geo_upload_deleted_events: %w[upload_id model_id],
gitlab_subscription_histories: %w[gitlab_subscription_id hosted_plan_id namespace_id],
identities: %w[user_id],
import_failures: %w[project_id],
@@ -66,7 +66,6 @@ RSpec.describe 'Database schema' do
oauth_access_grants: %w[resource_owner_id application_id],
oauth_access_tokens: %w[resource_owner_id application_id],
oauth_applications: %w[owner_id],
- open_project_tracker_data: %w[closed_status_id],
packages_build_infos: %w[pipeline_id],
packages_package_file_build_infos: %w[pipeline_id],
product_analytics_events_experimental: %w[event_id txn_id user_id],
@@ -210,7 +209,7 @@ RSpec.describe 'Database schema' do
# We are skipping GEO models for now as it adds up complexity
describe 'for jsonb columns' do
- it 'uses json schema validator' do
+ it 'uses json schema validator', :eager_load do
columns_name_with_jsonb.each do |hash|
next if models_by_table_name[hash["table_name"]].nil?
diff --git a/spec/experiments/change_continuous_onboarding_link_urls_experiment_spec.rb b/spec/experiments/change_continuous_onboarding_link_urls_experiment_spec.rb
new file mode 100644
index 00000000000..815aaf7c397
--- /dev/null
+++ b/spec/experiments/change_continuous_onboarding_link_urls_experiment_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ChangeContinuousOnboardingLinkUrlsExperiment, :snowplow do
+ before do
+ stub_experiments(change_continuous_onboarding_link_urls: 'control')
+ end
+
+ describe '#track' do
+ context 'when no namespace has been set' do
+ it 'tracks the action as normal' do
+ subject.track(:some_action)
+
+ expect_snowplow_event(
+ category: subject.name,
+ action: 'some_action',
+ namespace: nil,
+ context: [
+ {
+ schema: 'iglu:com.gitlab/gitlab_experiment/jsonschema/1-0-0',
+ data: an_instance_of(Hash)
+ }
+ ]
+ )
+ end
+ end
+
+ context 'when a namespace has been set' do
+ let_it_be(:namespace) { create(:namespace) }
+
+ before do
+ subject.namespace = namespace
+ end
+
+ it 'tracks the action and merges the namespace into the event args' do
+ subject.track(:some_action)
+
+ expect_snowplow_event(
+ category: subject.name,
+ action: 'some_action',
+ namespace: namespace,
+ context: [
+ {
+ schema: 'iglu:com.gitlab/gitlab_experiment/jsonschema/1-0-0',
+ data: an_instance_of(Hash)
+ }
+ ]
+ )
+ end
+ end
+ end
+end
diff --git a/spec/experiments/empty_repo_upload_experiment_spec.rb b/spec/experiments/empty_repo_upload_experiment_spec.rb
deleted file mode 100644
index 10cbedbe8ba..00000000000
--- a/spec/experiments/empty_repo_upload_experiment_spec.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe EmptyRepoUploadExperiment, :experiment do
- subject { described_class.new(project: project) }
-
- let(:project) { create(:project, :repository) }
-
- describe '#track_initial_write' do
- context 'when experiment is turned on' do
- before do
- stub_experiments(empty_repo_upload: :control)
- end
-
- it "tracks an event for the first commit on a project" do
- expect(subject).to receive(:commit_count_for).with(project, max_count: described_class::INITIAL_COMMIT_COUNT, experiment: 'empty_repo_upload').and_return(1)
-
- expect(subject).to receive(:track).with(:initial_write, project: project).and_call_original
-
- subject.track_initial_write
- end
-
- it "doesn't track an event for projects with a commit count more than 1" do
- expect(subject).to receive(:commit_count_for).and_return(2)
-
- expect(subject).not_to receive(:track)
-
- subject.track_initial_write
- end
-
- it "doesn't track if the project is older" do
- expect(project).to receive(:created_at).and_return(described_class::TRACKING_START_DATE - 1.minute)
-
- expect(subject).not_to receive(:track)
-
- subject.track_initial_write
- end
- end
-
- context 'when experiment is turned off' do
- it "doesn't track when we generally shouldn't" do
- expect(subject).not_to receive(:track)
-
- subject.track_initial_write
- end
- end
- end
-end
diff --git a/spec/factories/analytics/cycle_analytics/issue_stage_events.rb b/spec/factories/analytics/cycle_analytics/issue_stage_events.rb
new file mode 100644
index 00000000000..8ad88152611
--- /dev/null
+++ b/spec/factories/analytics/cycle_analytics/issue_stage_events.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :cycle_analytics_issue_stage_event, class: 'Analytics::CycleAnalytics::IssueStageEvent' do
+ sequence(:stage_event_hash_id) { |n| n }
+ sequence(:issue_id) { 0 }
+ sequence(:group_id) { 0 }
+ sequence(:project_id) { 0 }
+
+ start_event_timestamp { 3.weeks.ago.to_date }
+ end_event_timestamp { 2.weeks.ago.to_date }
+ end
+end
diff --git a/spec/factories/analytics/cycle_analytics/merge_request_stage_events.rb b/spec/factories/analytics/cycle_analytics/merge_request_stage_events.rb
new file mode 100644
index 00000000000..d8fa43b024f
--- /dev/null
+++ b/spec/factories/analytics/cycle_analytics/merge_request_stage_events.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :cycle_analytics_merge_request_stage_event, class: 'Analytics::CycleAnalytics::MergeRequestStageEvent' do
+ sequence(:stage_event_hash_id) { |n| n }
+ sequence(:merge_request_id) { 0 }
+ sequence(:group_id) { 0 }
+ sequence(:project_id) { 0 }
+
+ start_event_timestamp { 3.weeks.ago.to_date }
+ end_event_timestamp { 2.weeks.ago.to_date }
+ end
+end
diff --git a/spec/factories/authentication_event.rb b/spec/factories/authentication_event.rb
index ff539c6f5c4..e02698fac38 100644
--- a/spec/factories/authentication_event.rb
+++ b/spec/factories/authentication_event.rb
@@ -7,5 +7,13 @@ FactoryBot.define do
user_name { 'Jane Doe' }
ip_address { '127.0.0.1' }
result { :failed }
+
+ trait :successful do
+ result { :success }
+ end
+
+ trait :failed do
+ result { :failed }
+ end
end
end
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index 1108c606df3..98023334894 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -282,6 +282,12 @@ FactoryBot.define do
end
end
+ trait :unarchived_trace_artifact do
+ after(:create) do |build, evaluator|
+ create(:ci_job_artifact, :unarchived_trace_artifact, job: build)
+ end
+ end
+
trait :trace_with_duplicate_sections do
after(:create) do |build, evaluator|
trace = File.binread(
@@ -443,7 +449,7 @@ FactoryBot.define do
options do
{
image: { name: 'ruby:2.7', entrypoint: '/bin/sh' },
- services: ['postgres', { name: 'docker:stable-dind', entrypoint: '/bin/sh', command: 'sleep 30', alias: 'docker' }],
+ services: ['postgres', { name: 'docker:stable-dind', entrypoint: '/bin/sh', command: 'sleep 30', alias: 'docker' }, { name: 'mysql:latest', variables: { MYSQL_ROOT_PASSWORD: 'root123.' } }],
script: %w(echo),
after_script: %w(ls date),
artifacts: {
diff --git a/spec/factories/ci/job_artifacts.rb b/spec/factories/ci/job_artifacts.rb
index 2f4eb99a073..223de873a04 100644
--- a/spec/factories/ci/job_artifacts.rb
+++ b/spec/factories/ci/job_artifacts.rb
@@ -87,6 +87,17 @@ FactoryBot.define do
end
end
+ trait :unarchived_trace_artifact do
+ file_type { :trace }
+ file_format { :raw }
+
+ after(:build) do |artifact, evaluator|
+ file = double('file', path: '/path/to/job.log')
+ artifact.file = file
+ allow(artifact.file).to receive(:file).and_return(CarrierWave::SanitizedFile.new(file))
+ end
+ end
+
trait :junit do
file_type { :junit }
file_format { :gzip }
diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb
index ae3404a41a2..1d25964a4be 100644
--- a/spec/factories/ci/pipelines.rb
+++ b/spec/factories/ci/pipelines.rb
@@ -213,6 +213,14 @@ FactoryBot.define do
end
end
+ trait :with_persisted_artifacts do
+ status { :success }
+
+ after(:create) do |pipeline, evaluator|
+ pipeline.builds << create(:ci_build, :artifacts, pipeline: pipeline, project: pipeline.project)
+ end
+ end
+
trait :with_job do
after(:build) do |pipeline, evaluator|
pipeline.builds << build(:ci_build, pipeline: pipeline, project: pipeline.project)
diff --git a/spec/factories/ci/reports/security/findings.rb b/spec/factories/ci/reports/security/findings.rb
index e3971bc48f3..8a39fce971f 100644
--- a/spec/factories/ci/reports/security/findings.rb
+++ b/spec/factories/ci/reports/security/findings.rb
@@ -9,7 +9,7 @@ FactoryBot.define do
metadata_version { 'sast:1.0' }
name { 'Cipher with no integrity' }
report_type { :sast }
- raw_metadata do
+ original_data do
{
description: "The cipher does not provide data integrity update 1",
solution: "GCM mode introduces an HMAC into the resulting encrypted data, providing integrity of the result.",
@@ -26,7 +26,7 @@ FactoryBot.define do
url: "https://crypto.stackexchange.com/questions/31428/pbewithmd5anddes-cipher-does-not-check-for-integrity-first"
}
]
- }.to_json
+ }.deep_stringify_keys
end
scanner factory: :ci_reports_security_scanner
severity { :high }
diff --git a/spec/factories/ci/runner_namespaces.rb b/spec/factories/ci/runner_namespaces.rb
index a5060d196ca..e3cebed789b 100644
--- a/spec/factories/ci/runner_namespaces.rb
+++ b/spec/factories/ci/runner_namespaces.rb
@@ -2,7 +2,14 @@
FactoryBot.define do
factory :ci_runner_namespace, class: 'Ci::RunnerNamespace' do
- runner factory: [:ci_runner, :group]
group
+
+ after(:build) do |runner_namespace, evaluator|
+ unless runner_namespace.runner.present?
+ runner_namespace.runner = build(
+ :ci_runner, :group, runner_namespaces: [runner_namespace]
+ )
+ end
+ end
end
end
diff --git a/spec/factories/ci/runners.rb b/spec/factories/ci/runners.rb
index d0853df4e4b..6665b7b76a0 100644
--- a/spec/factories/ci/runners.rb
+++ b/spec/factories/ci/runners.rb
@@ -11,6 +11,7 @@ FactoryBot.define do
runner_type { :instance_type }
transient do
+ groups { [] }
projects { [] }
end
@@ -18,6 +19,10 @@ FactoryBot.define do
evaluator.projects.each do |proj|
runner.runner_projects << build(:ci_runner_project, project: proj)
end
+
+ evaluator.groups.each do |group|
+ runner.runner_namespaces << build(:ci_runner_namespace, namespace: group)
+ end
end
trait :online do
@@ -32,7 +37,9 @@ FactoryBot.define do
runner_type { :group_type }
after(:build) do |runner, evaluator|
- runner.groups << build(:group) if runner.groups.empty?
+ if runner.runner_namespaces.empty?
+ runner.runner_namespaces << build(:ci_runner_namespace)
+ end
end
end
diff --git a/spec/factories/customer_relations/issue_customer_relations_contacts.rb b/spec/factories/customer_relations/issue_customer_relations_contacts.rb
new file mode 100644
index 00000000000..6a4fecfb3cf
--- /dev/null
+++ b/spec/factories/customer_relations/issue_customer_relations_contacts.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :issue_customer_relations_contact, class: 'CustomerRelations::IssueContact' do
+ issue { association(:issue, project: project) }
+ contact { association(:contact, group: group) }
+
+ transient do
+ group { association(:group) }
+ project { association(:project, group: group) }
+ end
+
+ trait :for_contact do
+ issue { association(:issue, project: project) }
+ contact { raise ArgumentError, '`contact` is manadatory' }
+
+ transient do
+ project { association(:project, group: contact.group) }
+ end
+ end
+
+ trait :for_issue do
+ issue { raise ArgumentError, '`issue` is manadatory' }
+ contact { association(:contact, group: issue.project.group) }
+ end
+ end
+end
diff --git a/spec/factories/design_management/designs.rb b/spec/factories/design_management/designs.rb
index c23a67fe95b..56a1b55b969 100644
--- a/spec/factories/design_management/designs.rb
+++ b/spec/factories/design_management/designs.rb
@@ -39,7 +39,7 @@ FactoryBot.define do
sha = commit_version[action]
version = DesignManagement::Version.new(sha: sha, issue: issue, author: evaluator.author)
version.save!(validate: false) # We need it to have an ID, validate later
- Gitlab::Database.main.bulk_insert(dv_table_name, [action.row_attrs(version)]) # rubocop:disable Gitlab/BulkInsert
+ ApplicationRecord.legacy_bulk_insert(dv_table_name, [action.row_attrs(version)]) # rubocop:disable Gitlab/BulkInsert
end
# always a creation
diff --git a/spec/factories/error_tracking/error_event.rb b/spec/factories/error_tracking/error_event.rb
index 9620e3999d6..83f38150b11 100644
--- a/spec/factories/error_tracking/error_event.rb
+++ b/spec/factories/error_tracking/error_event.rb
@@ -63,5 +63,9 @@ FactoryBot.define do
level { 'error' }
occurred_at { Time.now.iso8601 }
payload { Gitlab::Json.parse(File.read(Rails.root.join('spec/fixtures/', 'error_tracking/parsed_event.json'))) }
+
+ trait :browser do
+ payload { Gitlab::Json.parse(File.read(Rails.root.join('spec/fixtures/', 'error_tracking/browser_event.json'))) }
+ end
end
end
diff --git a/spec/factories/gitlab/database/reindexing/queued_action.rb b/spec/factories/gitlab/database/reindexing/queued_action.rb
new file mode 100644
index 00000000000..30e12a81272
--- /dev/null
+++ b/spec/factories/gitlab/database/reindexing/queued_action.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :reindexing_queued_action, class: 'Gitlab::Database::Reindexing::QueuedAction' do
+ association :index, factory: :postgres_index
+
+ state { Gitlab::Database::Reindexing::QueuedAction.states[:queued] }
+ index_identifier { index.identifier }
+ end
+end
diff --git a/spec/factories/group_members.rb b/spec/factories/group_members.rb
index 37ddbc09616..ab2321c81c4 100644
--- a/spec/factories/group_members.rb
+++ b/spec/factories/group_members.rb
@@ -34,5 +34,18 @@ FactoryBot.define do
access_level { GroupMember::MINIMAL_ACCESS }
end
+
+ transient do
+ tasks_to_be_done { [] }
+ end
+
+ after(:build) do |group_member, evaluator|
+ if evaluator.tasks_to_be_done.present?
+ build(:member_task,
+ member: group_member,
+ project: build(:project, namespace: group_member.source),
+ tasks_to_be_done: evaluator.tasks_to_be_done)
+ end
+ end
end
end
diff --git a/spec/factories/integrations.rb b/spec/factories/integrations.rb
index 63f85c04ac7..76415f82ed0 100644
--- a/spec/factories/integrations.rb
+++ b/spec/factories/integrations.rb
@@ -111,6 +111,12 @@ FactoryBot.define do
end
end
+ factory :shimo_integration, class: 'Integrations::Shimo' do
+ project
+ active { true }
+ external_wiki_url { 'https://shimo.example.com/desktop' }
+ end
+
factory :confluence_integration, class: 'Integrations::Confluence' do
project
active { true }
@@ -216,6 +222,11 @@ FactoryBot.define do
template { true }
end
+ trait :group do
+ group
+ project { nil }
+ end
+
trait :instance do
project { nil }
instance { true }
diff --git a/spec/factories/member_tasks.rb b/spec/factories/member_tasks.rb
new file mode 100644
index 00000000000..133ccce5f8a
--- /dev/null
+++ b/spec/factories/member_tasks.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :member_task do
+ member { association(:group_member, :invited) }
+ project { association(:project, namespace: member.source) }
+ tasks_to_be_done { [:ci, :code] }
+ end
+end
diff --git a/spec/factories/namespaces/project_namespaces.rb b/spec/factories/namespaces/project_namespaces.rb
index ca9fc5f8768..6bf17088741 100644
--- a/spec/factories/namespaces/project_namespaces.rb
+++ b/spec/factories/namespaces/project_namespaces.rb
@@ -2,7 +2,7 @@
FactoryBot.define do
factory :project_namespace, class: 'Namespaces::ProjectNamespace' do
- project
+ association :project, factory: :project, strategy: :build
parent { project.namespace }
visibility_level { project.visibility_level }
name { project.name }
diff --git a/spec/factories/operations/feature_flags/strategy.rb b/spec/factories/operations/feature_flags/strategy.rb
index bdb5d9f0f3c..8d04b6d25aa 100644
--- a/spec/factories/operations/feature_flags/strategy.rb
+++ b/spec/factories/operations/feature_flags/strategy.rb
@@ -5,5 +5,37 @@ FactoryBot.define do
association :feature_flag, factory: :operations_feature_flag
name { "default" }
parameters { {} }
+
+ trait :default do
+ name { "default" }
+ parameters { {} }
+ end
+
+ trait :gitlab_userlist do
+ association :user_list, factory: :operations_feature_flag_user_list
+ name { "gitlabUserList" }
+ parameters { {} }
+ end
+
+ trait :flexible_rollout do
+ name { "flexibleRollout" }
+ parameters do
+ {
+ groupId: 'default',
+ rollout: '10',
+ stickiness: 'default'
+ }
+ end
+ end
+
+ trait :gradual_rollout do
+ name { "gradualRolloutUserId" }
+ parameters { { percentage: '10', groupId: 'default' } }
+ end
+
+ trait :userwithid do
+ name { "userWithId" }
+ parameters { { userIds: 'user1' } }
+ end
end
end
diff --git a/spec/factories/packages/helm/file_metadatum.rb b/spec/factories/packages/helm/file_metadatum.rb
index 3f599b5d5c0..590956e5d49 100644
--- a/spec/factories/packages/helm/file_metadatum.rb
+++ b/spec/factories/packages/helm/file_metadatum.rb
@@ -9,7 +9,11 @@ FactoryBot.define do
package_file { association(:helm_package_file, without_loaded_metadatum: true) }
sequence(:channel) { |n| "#{FFaker::Lorem.word}-#{n}" }
metadata do
- { 'name': package_file.package.name, 'version': package_file.package.version, 'apiVersion': 'v2' }.tap do |defaults|
+ {
+ 'name': package_file.package.name,
+ 'version': package_file.package.version,
+ 'apiVersion': 'v2'
+ }.tap do |defaults|
defaults['description'] = description if description
end
end
diff --git a/spec/factories/packages/npm/metadata.rb b/spec/factories/packages/npm/metadata.rb
new file mode 100644
index 00000000000..c8acaa10199
--- /dev/null
+++ b/spec/factories/packages/npm/metadata.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :npm_metadatum, class: 'Packages::Npm::Metadatum' do
+ package { association(:npm_package) }
+
+ package_json do
+ {
+ 'name': package.name,
+ 'version': package.version,
+ 'dist': {
+ 'tarball': 'http://localhost/tarball.tgz',
+ 'shasum': '1234567890'
+ }
+ }
+ end
+ end
+end
diff --git a/spec/factories/project_members.rb b/spec/factories/project_members.rb
index 3e83ab7118c..f2dedc178c7 100644
--- a/spec/factories/project_members.rb
+++ b/spec/factories/project_members.rb
@@ -23,5 +23,15 @@ FactoryBot.define do
trait :blocked do
after(:build) { |project_member, _| project_member.user.block! }
end
+
+ transient do
+ tasks_to_be_done { [] }
+ end
+
+ after(:build) do |project_member, evaluator|
+ if evaluator.tasks_to_be_done.present?
+ build(:member_task, member: project_member, project: project_member.source, tasks_to_be_done: evaluator.tasks_to_be_done)
+ end
+ end
end
end
diff --git a/spec/factories/user_highest_roles.rb b/spec/factories/user_highest_roles.rb
index 761a8b6c583..ee5794b55fb 100644
--- a/spec/factories/user_highest_roles.rb
+++ b/spec/factories/user_highest_roles.rb
@@ -5,10 +5,10 @@ FactoryBot.define do
highest_access_level { nil }
user
- trait(:guest) { highest_access_level { GroupMember::GUEST } }
- trait(:reporter) { highest_access_level { GroupMember::REPORTER } }
- trait(:developer) { highest_access_level { GroupMember::DEVELOPER } }
- trait(:maintainer) { highest_access_level { GroupMember::MAINTAINER } }
- trait(:owner) { highest_access_level { GroupMember::OWNER } }
+ trait(:guest) { highest_access_level { GroupMember::GUEST } }
+ trait(:reporter) { highest_access_level { GroupMember::REPORTER } }
+ trait(:developer) { highest_access_level { GroupMember::DEVELOPER } }
+ trait(:maintainer) { highest_access_level { GroupMember::MAINTAINER } }
+ trait(:owner) { highest_access_level { GroupMember::OWNER } }
end
end
diff --git a/spec/factories/users/credit_card_validations.rb b/spec/factories/users/credit_card_validations.rb
index 09940347708..509e86e7bd3 100644
--- a/spec/factories/users/credit_card_validations.rb
+++ b/spec/factories/users/credit_card_validations.rb
@@ -3,7 +3,10 @@
FactoryBot.define do
factory :credit_card_validation, class: 'Users::CreditCardValidation' do
user
-
- credit_card_validated_at { Time.current }
+ sequence(:credit_card_validated_at) { |n| Time.current + n }
+ expiration_date { 1.year.from_now.end_of_month }
+ last_digits { 10 }
+ holder_name { 'John Smith' }
+ network { 'AmericanExpress' }
end
end
diff --git a/spec/factories_spec.rb b/spec/factories_spec.rb
index 7dc38b25fac..811ed18dce3 100644
--- a/spec/factories_spec.rb
+++ b/spec/factories_spec.rb
@@ -22,6 +22,8 @@ RSpec.describe 'factories' do
[:debian_project_component_file, :object_storage],
[:debian_project_distribution, :object_storage],
[:debian_file_metadatum, :unknown],
+ [:issue_customer_relations_contact, :for_contact],
+ [:issue_customer_relations_contact, :for_issue],
[:package_file, :object_storage],
[:pages_domain, :without_certificate],
[:pages_domain, :without_key],
@@ -72,6 +74,8 @@ RSpec.describe 'factories' do
fork_network_member
group_member
import_state
+ issue_customer_relations_contact
+ member_task
milestone_release
namespace
project_broken_repo
diff --git a/spec/features/admin/admin_appearance_spec.rb b/spec/features/admin/admin_appearance_spec.rb
index cb69eac8035..0785c736cfb 100644
--- a/spec/features/admin/admin_appearance_spec.rb
+++ b/spec/features/admin/admin_appearance_spec.rb
@@ -94,7 +94,7 @@ RSpec.describe 'Admin Appearance' do
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
visit new_project_path
- find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
+ click_link 'Create blank project'
expect_custom_new_project_appearance(appearance)
end
diff --git a/spec/features/admin/admin_deploy_keys_spec.rb b/spec/features/admin/admin_deploy_keys_spec.rb
index c326d0fd741..53caf0fac33 100644
--- a/spec/features/admin/admin_deploy_keys_spec.rb
+++ b/spec/features/admin/admin_deploy_keys_spec.rb
@@ -3,11 +3,13 @@
require 'spec_helper'
RSpec.describe 'admin deploy keys' do
+ let_it_be(:admin) { create(:admin) }
+
let!(:deploy_key) { create(:deploy_key, public: true) }
let!(:another_deploy_key) { create(:another_deploy_key, public: true) }
before do
- admin = create(:admin)
+ stub_feature_flags(admin_deploy_keys_vue: false)
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
end
@@ -15,7 +17,7 @@ RSpec.describe 'admin deploy keys' do
it 'show all public deploy keys' do
visit admin_deploy_keys_path
- page.within(find('.deploy-keys-list', match: :first)) do
+ page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
expect(page).to have_content(deploy_key.title)
expect(page).to have_content(another_deploy_key.title)
end
@@ -26,7 +28,7 @@ RSpec.describe 'admin deploy keys' do
visit admin_deploy_keys_path
- page.within(find('.deploy-keys-list', match: :first)) do
+ page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
expect(page).to have_content(write_key.project.full_name)
end
end
@@ -46,7 +48,7 @@ RSpec.describe 'admin deploy keys' do
expect(current_path).to eq admin_deploy_keys_path
- page.within(find('.deploy-keys-list', match: :first)) do
+ page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
expect(page).to have_content('laptop')
end
end
@@ -64,7 +66,7 @@ RSpec.describe 'admin deploy keys' do
expect(current_path).to eq admin_deploy_keys_path
- page.within(find('.deploy-keys-list', match: :first)) do
+ page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
expect(page).to have_content('new-title')
end
end
@@ -79,9 +81,23 @@ RSpec.describe 'admin deploy keys' do
find('tr', text: deploy_key.title).click_link('Remove')
expect(current_path).to eq admin_deploy_keys_path
- page.within(find('.deploy-keys-list', match: :first)) do
+ page.within(find('[data-testid="deploy-keys-list"]', match: :first)) do
expect(page).not_to have_content(deploy_key.title)
end
end
end
+
+ context 'when `admin_deploy_keys_vue` feature flag is enabled', :js do
+ before do
+ stub_feature_flags(admin_deploy_keys_vue: true)
+
+ visit admin_deploy_keys_path
+ end
+
+ it 'renders the Vue app', :aggregate_failures do
+ expect(page).to have_content('Public deploy keys')
+ expect(page).to have_selector('[data-testid="deploy-keys-list"]')
+ expect(page).to have_link('New deploy key', href: new_admin_deploy_key_path)
+ end
+ end
end
diff --git a/spec/features/admin/admin_disables_two_factor_spec.rb b/spec/features/admin/admin_disables_two_factor_spec.rb
index 1f34c4ed17c..f65e85b4cb6 100644
--- a/spec/features/admin/admin_disables_two_factor_spec.rb
+++ b/spec/features/admin/admin_disables_two_factor_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'Admin disables 2FA for a user' do
it 'successfully', :js do
+ stub_feature_flags(bootstrap_confirmation_modals: false)
admin = create(:admin)
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb
index 8315b8f44b0..8d4e7a7442c 100644
--- a/spec/features/admin/admin_groups_spec.rb
+++ b/spec/features/admin/admin_groups_spec.rb
@@ -252,6 +252,7 @@ RSpec.describe 'Admin Groups' do
describe 'admin remove themself from a group', :js, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/222342' do
it 'removes admin from the group' do
+ stub_feature_flags(bootstrap_confirmation_modals: false)
group.add_user(current_user, Gitlab::Access::DEVELOPER)
visit group_group_members_path(group)
diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb
index a501efd82ed..32e4d18227e 100644
--- a/spec/features/admin/admin_hooks_spec.rb
+++ b/spec/features/admin/admin_hooks_spec.rb
@@ -79,6 +79,7 @@ RSpec.describe 'Admin::Hooks' do
let(:hook_url) { generate(:url) }
before do
+ stub_feature_flags(bootstrap_confirmation_modals: false)
create(:system_hook, url: hook_url)
end
diff --git a/spec/features/admin/admin_labels_spec.rb b/spec/features/admin/admin_labels_spec.rb
index 08d81906d9f..65de1160cfd 100644
--- a/spec/features/admin/admin_labels_spec.rb
+++ b/spec/features/admin/admin_labels_spec.rb
@@ -14,6 +14,7 @@ RSpec.describe 'admin issues labels' do
describe 'list' do
before do
+ stub_feature_flags(bootstrap_confirmation_modals: false)
visit admin_labels_path
end
diff --git a/spec/features/admin/admin_manage_applications_spec.rb b/spec/features/admin/admin_manage_applications_spec.rb
index b6437fce540..4cf290293bd 100644
--- a/spec/features/admin/admin_manage_applications_spec.rb
+++ b/spec/features/admin/admin_manage_applications_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe 'admin manage applications' do
let_it_be(:new_application_path) { new_admin_application_path }
let_it_be(:applications_path) { admin_applications_path }
+ let_it_be(:index_path) { admin_applications_path }
before do
admin = create(:admin)
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index 8053be89ffc..7e2751daefa 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -24,40 +24,37 @@ RSpec.describe "Admin Runners" do
visit admin_runners_path
- expect(page).to have_text "Set up a shared runner manually"
+ expect(page).to have_text "Register an instance runner"
expect(page).to have_text "Runners currently online: 1"
end
- it 'with an instance runner shows an instance badge and no project count' do
+ it 'with an instance runner shows an instance badge' do
runner = create(:ci_runner, :instance)
visit admin_runners_path
within "[data-testid='runner-row-#{runner.id}']" do
expect(page).to have_selector '.badge', text: 'shared'
- expect(page).to have_text 'n/a'
end
end
- it 'with a group runner shows a group badge and no project count' do
+ it 'with a group runner shows a group badge' do
runner = create(:ci_runner, :group, groups: [group])
visit admin_runners_path
within "[data-testid='runner-row-#{runner.id}']" do
expect(page).to have_selector '.badge', text: 'group'
- expect(page).to have_text 'n/a'
end
end
- it 'with a project runner shows a project badge and project count' do
+ it 'with a project runner shows a project badge' do
runner = create(:ci_runner, :project, projects: [project])
visit admin_runners_path
within "[data-testid='runner-row-#{runner.id}']" do
expect(page).to have_selector '.badge', text: 'specific'
- expect(page).to have_text '1'
end
end
@@ -69,6 +66,13 @@ RSpec.describe "Admin Runners" do
visit admin_runners_path
end
+ it 'runner types tabs have total counts and can be selected' do
+ expect(page).to have_link('All 2')
+ expect(page).to have_link('Instance 2')
+ expect(page).to have_link('Group 0')
+ expect(page).to have_link('Project 0')
+ end
+
it 'shows runners' do
expect(page).to have_content("runner-foo")
expect(page).to have_content("runner-bar")
@@ -137,6 +141,19 @@ RSpec.describe "Admin Runners" do
expect(page).not_to have_content 'runner-b-1'
expect(page).not_to have_content 'runner-a-2'
end
+
+ it 'shows correct runner when type is selected and search term is entered' do
+ create(:ci_runner, :instance, description: 'runner-connected', contacted_at: Time.now)
+ create(:ci_runner, :instance, description: 'runner-not-connected', contacted_at: nil)
+
+ visit admin_runners_path
+
+ # use the string "Not" to avoid using space and trigger an early selection
+ input_filtered_search_filter_is_only('Status', 'Not')
+
+ expect(page).not_to have_content 'runner-connected'
+ expect(page).to have_content 'runner-not-connected'
+ end
end
describe 'filter by type' do
@@ -145,13 +162,25 @@ RSpec.describe "Admin Runners" do
create(:ci_runner, :group, description: 'runner-group', groups: [group])
end
+ it '"All" tab is selected by default' do
+ visit admin_runners_path
+
+ page.within('[data-testid="runner-type-tabs"]') do
+ expect(page).to have_link('All', class: 'active')
+ end
+ end
+
it 'shows correct runner when type matches' do
visit admin_runners_path
expect(page).to have_content 'runner-project'
expect(page).to have_content 'runner-group'
- input_filtered_search_filter_is_only('Type', 'project')
+ page.within('[data-testid="runner-type-tabs"]') do
+ click_on('Project')
+
+ expect(page).to have_link('Project', class: 'active')
+ end
expect(page).to have_content 'runner-project'
expect(page).not_to have_content 'runner-group'
@@ -160,7 +189,11 @@ RSpec.describe "Admin Runners" do
it 'shows no runner when type does not match' do
visit admin_runners_path
- input_filtered_search_filter_is_only('Type', 'instance')
+ page.within('[data-testid="runner-type-tabs"]') do
+ click_on 'Instance'
+
+ expect(page).to have_link('Instance', class: 'active')
+ end
expect(page).not_to have_content 'runner-project'
expect(page).not_to have_content 'runner-group'
@@ -173,7 +206,9 @@ RSpec.describe "Admin Runners" do
visit admin_runners_path
- input_filtered_search_filter_is_only('Type', 'project')
+ page.within('[data-testid="runner-type-tabs"]') do
+ click_on 'Project'
+ end
expect(page).to have_content 'runner-project'
expect(page).to have_content 'runner-2-project'
@@ -185,6 +220,26 @@ RSpec.describe "Admin Runners" do
expect(page).not_to have_content 'runner-2-project'
expect(page).not_to have_content 'runner-group'
end
+
+ it 'maintains the same filter when switching between runner types' do
+ create(:ci_runner, :project, description: 'runner-paused-project', active: false, projects: [project])
+
+ visit admin_runners_path
+
+ input_filtered_search_filter_is_only('Status', 'Active')
+
+ expect(page).to have_content 'runner-project'
+ expect(page).to have_content 'runner-group'
+ expect(page).not_to have_content 'runner-paused-project'
+
+ page.within('[data-testid="runner-type-tabs"]') do
+ click_on 'Project'
+ end
+
+ expect(page).to have_content 'runner-project'
+ expect(page).not_to have_content 'runner-group'
+ expect(page).not_to have_content 'runner-paused-project'
+ end
end
describe 'filter by tag' do
@@ -267,29 +322,55 @@ RSpec.describe "Admin Runners" do
end
it 'has all necessary texts including no runner message' do
- expect(page).to have_text "Set up a shared runner manually"
+ expect(page).to have_text "Register an instance runner"
expect(page).to have_text "Runners currently online: 0"
expect(page).to have_text 'No runners found'
end
end
- describe 'runners registration token' do
+ describe 'runners registration' do
let!(:token) { Gitlab::CurrentSettings.runners_registration_token }
before do
visit admin_runners_path
+
+ click_on 'Register an instance runner'
+ end
+
+ describe 'show registration instructions' do
+ before do
+ click_on 'Show runner installation and registration instructions'
+
+ wait_for_requests
+ end
+
+ it 'opens runner installation modal' do
+ expect(page).to have_text "Install a runner"
+
+ expect(page).to have_text "Environment"
+ expect(page).to have_text "Architecture"
+ expect(page).to have_text "Download and install binary"
+ end
+
+ it 'dismisses runner installation modal' do
+ page.within('[role="dialog"]') do
+ click_button('Close', match: :first)
+ end
+
+ expect(page).not_to have_text "Install a runner"
+ end
end
it 'has a registration token' do
click_on 'Click to reveal'
- expect(page.find('[data-testid="registration-token"]')).to have_content(token)
+ expect(page.find('[data-testid="token-value"]')).to have_content(token)
end
describe 'reset registration token' do
- let(:page_token) { find('[data-testid="registration-token"]').text }
+ let(:page_token) { find('[data-testid="token-value"]').text }
before do
- click_button 'Reset registration token'
+ click_on 'Reset registration token'
page.accept_alert
@@ -297,6 +378,8 @@ RSpec.describe "Admin Runners" do
end
it 'changes registration token' do
+ click_on 'Register an instance runner'
+
click_on 'Click to reveal'
expect(page_token).not_to eq token
end
diff --git a/spec/features/admin/admin_sees_project_statistics_spec.rb b/spec/features/admin/admin_sees_project_statistics_spec.rb
index 3433cc01b8e..9d9217c4574 100644
--- a/spec/features/admin/admin_sees_project_statistics_spec.rb
+++ b/spec/features/admin/admin_sees_project_statistics_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe "Admin > Admin sees project statistics" do
let(:project) { create(:project, :repository) }
it "shows project statistics" do
- expect(page).to have_content("Storage: 0 Bytes (Repository: 0 Bytes / Wikis: 0 Bytes / Build Artifacts: 0 Bytes / LFS: 0 Bytes / Snippets: 0 Bytes / Packages: 0 Bytes / Uploads: 0 Bytes)")
+ expect(page).to have_content("Storage: 0 Bytes (Repository: 0 Bytes / Wikis: 0 Bytes / Build Artifacts: 0 Bytes / Pipeline Artifacts: 0 Bytes / LFS: 0 Bytes / Snippets: 0 Bytes / Packages: 0 Bytes / Uploads: 0 Bytes)")
end
end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index 1c50a7f891f..0a39baca259 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -491,22 +491,22 @@ RSpec.describe 'Admin updates settings' do
group = create(:group)
page.within('.as-performance-bar') do
- check 'Allow non-administrators to access to the performance bar'
+ check 'Allow non-administrators access to the performance bar'
fill_in 'Allow access to members of the following group', with: group.path
click_on 'Save changes'
end
expect(page).to have_content "Application settings saved successfully"
- expect(find_field('Allow non-administrators to access to the performance bar')).to be_checked
+ expect(find_field('Allow non-administrators access to the performance bar')).to be_checked
expect(find_field('Allow access to members of the following group').value).to eq group.path
page.within('.as-performance-bar') do
- uncheck 'Allow non-administrators to access to the performance bar'
+ uncheck 'Allow non-administrators access to the performance bar'
click_on 'Save changes'
end
expect(page).to have_content 'Application settings saved successfully'
- expect(find_field('Allow non-administrators to access to the performance bar')).not_to be_checked
+ expect(find_field('Allow non-administrators access to the performance bar')).not_to be_checked
expect(find_field('Allow access to members of the following group').value).to be_nil
end
diff --git a/spec/features/admin/admin_users_impersonation_tokens_spec.rb b/spec/features/admin/admin_users_impersonation_tokens_spec.rb
index ed8ea84fbf8..6643ebe82e6 100644
--- a/spec/features/admin/admin_users_impersonation_tokens_spec.rb
+++ b/spec/features/admin/admin_users_impersonation_tokens_spec.rb
@@ -74,6 +74,7 @@ RSpec.describe 'Admin > Users > Impersonation Tokens', :js do
let!(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) }
it "allows revocation of an active impersonation token" do
+ stub_feature_flags(bootstrap_confirmation_modals: false)
visit admin_user_impersonation_tokens_path(user_id: user.username)
accept_confirm { click_on "Revoke" }
diff --git a/spec/features/admin/admin_uses_repository_checks_spec.rb b/spec/features/admin/admin_uses_repository_checks_spec.rb
index 0e448446085..c13313609b5 100644
--- a/spec/features/admin/admin_uses_repository_checks_spec.rb
+++ b/spec/features/admin/admin_uses_repository_checks_spec.rb
@@ -8,6 +8,7 @@ RSpec.describe 'Admin uses repository checks', :request_store do
let(:admin) { create(:admin) }
before do
+ stub_feature_flags(bootstrap_confirmation_modals: false)
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
sign_in(admin)
end
diff --git a/spec/features/admin/clusters/eks_spec.rb b/spec/features/admin/clusters/eks_spec.rb
index a1bac720349..bb2678de2ae 100644
--- a/spec/features/admin/clusters/eks_spec.rb
+++ b/spec/features/admin/clusters/eks_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe 'Instance-level AWS EKS Cluster', :js do
before do
visit admin_clusters_path
- click_link 'Integrate with a cluster certificate'
+ click_link 'Connect with a certificate'
end
context 'when user creates a cluster on AWS EKS' do
diff --git a/spec/features/admin/users/user_spec.rb b/spec/features/admin/users/user_spec.rb
index 624bfde7359..73477fb93dd 100644
--- a/spec/features/admin/users/user_spec.rb
+++ b/spec/features/admin/users/user_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe 'Admin::Users::User' do
let_it_be(:current_user) { create(:admin) }
before do
+ stub_feature_flags(bootstrap_confirmation_modals: false)
sign_in(current_user)
gitlab_enable_admin_mode_sign_in(current_user)
end
diff --git a/spec/features/admin/users/users_spec.rb b/spec/features/admin/users/users_spec.rb
index 119b01ff552..fa943245fcb 100644
--- a/spec/features/admin/users/users_spec.rb
+++ b/spec/features/admin/users/users_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe 'Admin::Users' do
let_it_be(:current_user) { create(:admin) }
before do
+ stub_feature_flags(bootstrap_confirmation_modals: false)
sign_in(current_user)
gitlab_enable_admin_mode_sign_in(current_user)
end
@@ -164,7 +165,7 @@ RSpec.describe 'Admin::Users' do
visit admin_users_path
- page.within('.filter-two-factor-enabled small') do
+ page.within('.filter-two-factor-enabled .gl-tab-counter-badge') do
expect(page).to have_content('1')
end
end
@@ -181,7 +182,7 @@ RSpec.describe 'Admin::Users' do
it 'counts users who have not enabled 2FA' do
visit admin_users_path
- page.within('.filter-two-factor-disabled small') do
+ page.within('.filter-two-factor-disabled .gl-tab-counter-badge') do
expect(page).to have_content('2') # Including admin
end
end
@@ -200,7 +201,7 @@ RSpec.describe 'Admin::Users' do
visit admin_users_path
- page.within('.filter-blocked-pending-approval small') do
+ page.within('.filter-blocked-pending-approval .gl-tab-counter-badge') do
expect(page).to have_content('2')
end
end
diff --git a/spec/features/alert_management/alert_management_list_spec.rb b/spec/features/alert_management/alert_management_list_spec.rb
index 1e710169c9c..2fbce27033e 100644
--- a/spec/features/alert_management/alert_management_list_spec.rb
+++ b/spec/features/alert_management/alert_management_list_spec.rb
@@ -55,28 +55,4 @@ RSpec.describe 'Alert Management index', :js do
it_behaves_like 'alert page with title, filtered search, and table'
end
end
-
- describe 'managed_alerts_deprecation feature flag' do
- subject { page }
-
- before do
- stub_feature_flags(managed_alerts_deprecation: feature_flag_value)
- sign_in(developer)
-
- visit project_alert_management_index_path(project)
- wait_for_requests
- end
-
- context 'feature flag on' do
- let(:feature_flag_value) { true }
-
- it { is_expected.to have_pushed_frontend_feature_flags(managedAlertsDeprecation: true) }
- end
-
- context 'feature flag off' do
- let(:feature_flag_value) { false }
-
- it { is_expected.to have_pushed_frontend_feature_flags(managedAlertsDeprecation: false) }
- end
- end
end
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 9a5b5bbfc34..2f21961d1fc 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -536,6 +536,7 @@ RSpec.describe 'Project issue boards', :js do
let_it_be(:user_guest) { create(:user) }
before do
+ stub_feature_flags(bootstrap_confirmation_modals: false)
project.add_guest(user_guest)
sign_in(user_guest)
visit project_board_path(project, board)
diff --git a/spec/features/clusters/create_agent_spec.rb b/spec/features/clusters/create_agent_spec.rb
new file mode 100644
index 00000000000..f40932c4750
--- /dev/null
+++ b/spec/features/clusters/create_agent_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Cluster agent registration', :js do
+ let_it_be(:project) { create(:project, :custom_repo, files: { '.gitlab/agents/example-agent-1/config.yaml' => '' }) }
+ let_it_be(:current_user) { create(:user, maintainer_projects: [project]) }
+
+ before do
+ allow(Gitlab::Kas).to receive(:enabled?).and_return(true)
+ allow(Gitlab::Kas).to receive(:internal_url).and_return('kas.example.internal')
+
+ allow_next_instance_of(Gitlab::Kas::Client) do |client|
+ allow(client).to receive(:list_agent_config_files).and_return([
+ double(agent_name: 'example-agent-1', path: '.gitlab/agents/example-agent-1/config.yaml'),
+ double(agent_name: 'example-agent-2', path: '.gitlab/agents/example-agent-2/config.yaml')
+ ])
+ end
+
+ allow(Devise).to receive(:friendly_token).and_return('example-agent-token')
+
+ sign_in(current_user)
+ visit project_clusters_path(project)
+ end
+
+ it 'allows the user to select an agent to install, and displays the resulting agent token' do
+ click_button('Actions')
+ expect(page).to have_content('Install new Agent')
+
+ click_button('Select an Agent')
+ click_button('example-agent-2')
+ click_button('Register Agent')
+
+ expect(page).to have_content('The token value will not be shown again after you close this window.')
+ expect(page).to have_content('example-agent-token')
+ expect(page).to have_content('docker run --pull=always --rm')
+
+ within find('.modal-footer') do
+ click_button('Close')
+ end
+
+ expect(page).to have_link('example-agent-2')
+ end
+end
diff --git a/spec/features/contextual_sidebar_spec.rb b/spec/features/contextual_sidebar_spec.rb
index 39881a28b11..29c7e0ddd21 100644
--- a/spec/features/contextual_sidebar_spec.rb
+++ b/spec/features/contextual_sidebar_spec.rb
@@ -3,35 +3,110 @@
require 'spec_helper'
RSpec.describe 'Contextual sidebar', :js do
- let_it_be(:project) { create(:project) }
+ context 'when context is a project' do
+ let_it_be(:project) { create(:project) }
- let(:user) { project.owner }
+ let(:user) { project.owner }
- before do
- sign_in(user)
+ before do
+ sign_in(user)
+ end
- visit project_path(project)
- end
+ context 'when analyzing the menu' do
+ before do
+ visit project_path(project)
+ end
+
+ it 'shows flyout navs when collapsed or expanded apart from on the active item when expanded', :aggregate_failures do
+ expect(page).not_to have_selector('.js-sidebar-collapsed')
+
+ find('.rspec-link-pipelines').hover
+
+ expect(page).to have_selector('.is-showing-fly-out')
+
+ find('.rspec-project-link').hover
+
+ expect(page).not_to have_selector('.is-showing-fly-out')
+
+ find('.rspec-toggle-sidebar').click
+
+ find('.rspec-link-pipelines').hover
+
+ expect(page).to have_selector('.is-showing-fly-out')
- it 'shows flyout navs when collapsed or expanded apart from on the active item when expanded', :aggregate_failures do
- expect(page).not_to have_selector('.js-sidebar-collapsed')
+ find('.rspec-project-link').hover
+
+ expect(page).to have_selector('.is-showing-fly-out')
+ end
+ end
+
+ context 'with invite_members_in_side_nav experiment', :experiment do
+ it 'allows opening of modal for the candidate experience' do
+ stub_experiments(invite_members_in_side_nav: :candidate)
+ expect(experiment(:invite_members_in_side_nav)).to track(:assignment)
+ .with_context(group: project.group)
+ .on_next_instance
+
+ visit project_path(project)
+
+ page.within '[data-test-id="side-nav-invite-members"' do
+ find('[data-test-id="invite-members-button"').click
+ end
+
+ expect(page).to have_content("You're inviting members to the")
+ end
+
+ it 'does not have invite members link in side nav for the control experience' do
+ stub_experiments(invite_members_in_side_nav: :control)
+ expect(experiment(:invite_members_in_side_nav)).to track(:assignment)
+ .with_context(group: project.group)
+ .on_next_instance
+
+ visit project_path(project)
+
+ expect(page).not_to have_css('[data-test-id="side-nav-invite-members"')
+ end
+ end
+ end
- find('.rspec-link-pipelines').hover
+ context 'when context is a group' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) do
+ create(:group).tap do |g|
+ g.add_owner(user)
+ end
+ end
- expect(page).to have_selector('.is-showing-fly-out')
+ before do
+ sign_in(user)
+ end
- find('.rspec-project-link').hover
+ context 'with invite_members_in_side_nav experiment', :experiment do
+ it 'allows opening of modal for the candidate experience' do
+ stub_experiments(invite_members_in_side_nav: :candidate)
+ expect(experiment(:invite_members_in_side_nav)).to track(:assignment)
+ .with_context(group: group)
+ .on_next_instance
- expect(page).not_to have_selector('.is-showing-fly-out')
+ visit group_path(group)
- find('.rspec-toggle-sidebar').click
+ page.within '[data-test-id="side-nav-invite-members"' do
+ find('[data-test-id="invite-members-button"').click
+ end
- find('.rspec-link-pipelines').hover
+ expect(page).to have_content("You're inviting members to the")
+ end
- expect(page).to have_selector('.is-showing-fly-out')
+ it 'does not have invite members link in side nav for the control experience' do
+ stub_experiments(invite_members_in_side_nav: :control)
+ expect(experiment(:invite_members_in_side_nav)).to track(:assignment)
+ .with_context(group: group)
+ .on_next_instance
- find('.rspec-project-link').hover
+ visit group_path(group)
- expect(page).to have_selector('.is-showing-fly-out')
+ expect(page).not_to have_css('[data-test-id="side-nav-invite-members"')
+ end
+ end
end
end
diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb
index 34a55118cb3..69361f66a71 100644
--- a/spec/features/cycle_analytics_spec.rb
+++ b/spec/features/cycle_analytics_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe 'Value Stream Analytics', :js do
let_it_be(:user) { create(:user) }
let_it_be(:guest) { create(:user) }
let_it_be(:stage_table_selector) { '[data-testid="vsa-stage-table"]' }
+ let_it_be(:stage_filter_bar) { '[data-testid="vsa-filter-bar"]' }
let_it_be(:stage_table_event_selector) { '[data-testid="vsa-stage-event"]' }
let_it_be(:stage_table_event_title_selector) { '[data-testid="vsa-stage-event-title"]' }
let_it_be(:stage_table_pagination_selector) { '[data-testid="vsa-stage-pagination"]' }
@@ -27,9 +28,16 @@ RSpec.describe 'Value Stream Analytics', :js do
def set_daterange(from_date, to_date)
page.find(".js-daterange-picker-from input").set(from_date)
page.find(".js-daterange-picker-to input").set(to_date)
+
+ # simulate a blur event
+ page.find(".js-daterange-picker-to input").send_keys(:tab)
wait_for_all_requests
end
+ before do
+ stub_feature_flags(use_vsa_aggregated_tables: false)
+ end
+
context 'as an allowed user' do
context 'when project is new' do
before do
@@ -97,7 +105,7 @@ RSpec.describe 'Value Stream Analytics', :js do
end
end
- it 'shows data on each stage', :sidekiq_might_not_need_inline do
+ it 'shows data on each stage', :sidekiq_might_not_need_inline, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338332' do
expect_issue_to_be_present
click_stage('Plan')
@@ -133,7 +141,7 @@ RSpec.describe 'Value Stream Analytics', :js do
expect(metrics_values).to eq(['-'] * 4)
end
- it 'can sort records' do
+ it 'can sort records', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338332' do
# NOTE: checking that the string changes should suffice
# depending on the order the tests are run we might run into problems with hard coded strings
original_first_title = first_stage_title
@@ -158,6 +166,18 @@ RSpec.describe 'Value Stream Analytics', :js do
expect(page).not_to have_text(original_first_title, exact: true)
end
+ it 'can navigate directly to a value stream stream stage with filters applied' do
+ visit project_cycle_analytics_path(project, created_before: '2019-12-31', created_after: '2019-11-01', stage_id: 'code', milestone_title: milestone.title)
+ wait_for_requests
+
+ expect(page).to have_selector('.gl-path-active-item-indigo', text: 'Code')
+ expect(page.find(".js-daterange-picker-from input").value).to eq("2019-11-01")
+ expect(page.find(".js-daterange-picker-to input").value).to eq("2019-12-31")
+
+ filter_bar = page.find(stage_filter_bar)
+ expect(filter_bar.find(".gl-filtered-search-token-data-content").text).to eq("%#{milestone.title}")
+ end
+
def stage_time_column
stage_table.find(stage_table_duration_column_header_selector).ancestor("th")
end
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
index 27419479479..82288a6c1a6 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -80,7 +80,7 @@ RSpec.describe 'Dashboard Projects' do
visit dashboard_projects_path
expect(page).to have_content(project.name)
- expect(find('.nav-links li:nth-child(1) .badge-pill')).to have_content(1)
+ expect(find('.gl-tabs-nav li:nth-child(1) .badge-pill')).to have_content(1)
end
it 'shows personal projects on personal projects tab', :js do
@@ -128,8 +128,8 @@ RSpec.describe 'Dashboard Projects' do
expect(page).not_to have_content(project.name)
expect(page).to have_content(project2.name)
- expect(find('.nav-links li:nth-child(1) .badge-pill')).to have_content(1)
- expect(find('.nav-links li:nth-child(2) .badge-pill')).to have_content(1)
+ expect(find('.gl-tabs-nav li:nth-child(1) .badge-pill')).to have_content(1)
+ expect(find('.gl-tabs-nav li:nth-child(2) .badge-pill')).to have_content(1)
end
it 'does not show tabs to filter by all projects or personal' do
@@ -204,7 +204,7 @@ RSpec.describe 'Dashboard Projects' do
visit dashboard_projects_path
expect(page).to have_selector('[data-testid="project_topic_list"]')
- expect(page).to have_link('topic1', href: explore_projects_path(topic: 'topic1'))
+ expect(page).to have_link('topic1', href: topic_explore_projects_path(topic_name: 'topic1'))
end
end
diff --git a/spec/features/explore/topics_spec.rb b/spec/features/explore/topics_spec.rb
new file mode 100644
index 00000000000..9d2e76bc3a1
--- /dev/null
+++ b/spec/features/explore/topics_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Explore Topics' do
+ context 'when no topics exist' do
+ it 'renders empty message', :aggregate_failures do
+ visit topics_explore_projects_path
+
+ expect(current_path).to eq topics_explore_projects_path
+ expect(page).to have_content('There are no topics to show.')
+ end
+ end
+
+ context 'when topics exist' do
+ let!(:topic) { create(:topic, name: 'topic1') }
+
+ it 'renders topic list' do
+ visit topics_explore_projects_path
+
+ expect(current_path).to eq topics_explore_projects_path
+ expect(page).to have_content('topic1')
+ end
+ end
+end
diff --git a/spec/features/graphql_known_operations_spec.rb b/spec/features/graphql_known_operations_spec.rb
new file mode 100644
index 00000000000..ef406f12902
--- /dev/null
+++ b/spec/features/graphql_known_operations_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# We need to distinguish between known and unknown GraphQL operations. This spec
+# tests that we set up Gitlab::Graphql::KnownOperations.default which requires
+# integration of FE queries, webpack plugin, and BE.
+RSpec.describe 'Graphql known operations', :js do
+ around do |example|
+ # Let's make sure we aren't receiving or leaving behind any side-effects
+ # https://gitlab.com/gitlab-org/gitlab/-/jobs/1743294100
+ ::Gitlab::Graphql::KnownOperations.instance_variable_set(:@default, nil)
+ ::Gitlab::Webpack::GraphqlKnownOperations.clear_memoization!
+
+ example.run
+
+ ::Gitlab::Graphql::KnownOperations.instance_variable_set(:@default, nil)
+ ::Gitlab::Webpack::GraphqlKnownOperations.clear_memoization!
+ end
+
+ it 'collects known Graphql operations from the code', :aggregate_failures do
+ # Check that we include some arbitrary operation name we expect
+ known_operations = Gitlab::Graphql::KnownOperations.default.operations.map(&:name)
+
+ expect(known_operations).to include("searchProjects")
+ expect(known_operations.length).to be > 20
+ expect(known_operations).to all( match(%r{^[a-z]+}i) )
+ end
+end
diff --git a/spec/features/groups/clusters/eks_spec.rb b/spec/features/groups/clusters/eks_spec.rb
index c361c502cbb..fe62efbd3bf 100644
--- a/spec/features/groups/clusters/eks_spec.rb
+++ b/spec/features/groups/clusters/eks_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe 'Group AWS EKS Cluster', :js do
before do
visit group_clusters_path(group)
- click_link 'Integrate with a cluster certificate'
+ click_link 'Connect with a certificate'
end
context 'when user creates a cluster on AWS EKS' do
diff --git a/spec/features/groups/clusters/user_spec.rb b/spec/features/groups/clusters/user_spec.rb
index 2a7ededa39b..1788167c94c 100644
--- a/spec/features/groups/clusters/user_spec.rb
+++ b/spec/features/groups/clusters/user_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe 'User Cluster', :js do
before do
visit group_clusters_path(group)
- click_link 'Integrate with a cluster certificate'
+ click_link 'Connect with a certificate'
click_link 'Connect existing cluster'
end
@@ -129,7 +129,7 @@ RSpec.describe 'User Cluster', :js do
it 'user sees creation form with the successful message' do
expect(page).to have_content('Kubernetes cluster integration was successfully removed.')
- expect(page).to have_link('Integrate with a cluster certificate')
+ expect(page).to have_link('Connect with a certificate')
end
end
end
diff --git a/spec/features/groups/dependency_proxy_spec.rb b/spec/features/groups/dependency_proxy_spec.rb
index d6b0bdc8ea4..623fb065bfc 100644
--- a/spec/features/groups/dependency_proxy_spec.rb
+++ b/spec/features/groups/dependency_proxy_spec.rb
@@ -56,9 +56,14 @@ RSpec.describe 'Group Dependency Proxy' do
visit settings_path
wait_for_requests
- click_button 'Enable Proxy'
+ proxy_toggle = find('[data-testid="dependency-proxy-setting-toggle"]')
+ proxy_toggle_button = proxy_toggle.find('button')
- expect(page).to have_button 'Enable Proxy', class: '!is-checked'
+ expect(proxy_toggle).to have_css("button.is-checked")
+
+ proxy_toggle_button.click
+
+ expect(proxy_toggle).not_to have_css("button.is-checked")
visit path
diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb
index 489beb70ab3..4e59ab40d04 100644
--- a/spec/features/groups/issues_spec.rb
+++ b/spec/features/groups/issues_spec.rb
@@ -83,6 +83,18 @@ RSpec.describe 'Group issues page' do
end
end
+ it 'truncates issue counts if over the threshold', :clean_gitlab_redis_cache do
+ allow(Rails.cache).to receive(:read).and_call_original
+ allow(Rails.cache).to receive(:read).with(
+ ['group', group.id, 'issues'],
+ { expires_in: Gitlab::IssuablesCountForState::CACHE_EXPIRES_IN }
+ ).and_return({ opened: 1050, closed: 500, all: 1550 })
+
+ visit issues_group_path(group)
+
+ expect(page).to have_text('Open 1.1k Closed 500 All 1.6k')
+ end
+
context 'when project is archived' do
before do
::Projects::UpdateService.new(project, user_in_group, archived: true).execute
@@ -94,41 +106,6 @@ RSpec.describe 'Group issues page' do
expect(page).not_to have_content issue.title[0..80]
end
end
-
- context 'when cached issues state count is enabled', :clean_gitlab_redis_cache do
- before do
- stub_feature_flags(cached_issues_state_count: true)
- end
-
- it 'truncates issue counts if over the threshold' do
- allow(Rails.cache).to receive(:read).and_call_original
- allow(Rails.cache).to receive(:read).with(
- ['group', group.id, 'issues'],
- { expires_in: Gitlab::IssuablesCountForState::CACHE_EXPIRES_IN }
- ).and_return({ opened: 1050, closed: 500, all: 1550 })
-
- visit issues_group_path(group)
-
- expect(page).to have_text('Open 1.1k Closed 500 All 1.6k')
- end
- end
-
- context 'when cached issues state count is disabled', :clean_gitlab_redis_cache do
- before do
- stub_feature_flags(cached_issues_state_count: false)
- end
-
- it 'does not truncate counts if they are over the threshold' do
- allow_next_instance_of(IssuesFinder) do |finder|
- allow(finder).to receive(:count_by_state).and_return(true)
- .and_return({ opened: 1050, closed: 500, all: 1550 })
- end
-
- visit issues_group_path(group)
-
- expect(page).to have_text('Open 1,050 Closed 500 All 1,550')
- end
- end
end
context 'projects with issues disabled' do
diff --git a/spec/features/groups/labels/subscription_spec.rb b/spec/features/groups/labels/subscription_spec.rb
index dedded777ac..231c4b33bee 100644
--- a/spec/features/groups/labels/subscription_spec.rb
+++ b/spec/features/groups/labels/subscription_spec.rb
@@ -71,7 +71,7 @@ RSpec.describe 'Labels subscription' do
end
it 'does not show subscribed tab' do
- page.within('.nav-tabs') do
+ page.within('.gl-tabs-nav') do
expect(page).not_to have_link 'Subscribed'
end
end
@@ -86,7 +86,7 @@ RSpec.describe 'Labels subscription' do
end
def click_subscribed_tab
- page.within('.nav-tabs') do
+ page.within('.gl-tabs-nav') do
click_link 'Subscribed'
end
end
diff --git a/spec/features/groups/members/leave_group_spec.rb b/spec/features/groups/members/leave_group_spec.rb
index b73313745e9..e6bf1ffc2f7 100644
--- a/spec/features/groups/members/leave_group_spec.rb
+++ b/spec/features/groups/members/leave_group_spec.rb
@@ -10,6 +10,7 @@ RSpec.describe 'Groups > Members > Leave group' do
let(:group) { create(:group) }
before do
+ stub_feature_flags(bootstrap_confirmation_modals: false)
sign_in(user)
end
diff --git a/spec/features/groups/navbar_spec.rb b/spec/features/groups/navbar_spec.rb
index 0a159056569..22409e9e7f6 100644
--- a/spec/features/groups/navbar_spec.rb
+++ b/spec/features/groups/navbar_spec.rb
@@ -15,6 +15,7 @@ RSpec.describe 'Group navbar' do
insert_package_nav(_('Kubernetes'))
stub_feature_flags(group_iterations: false)
+ stub_feature_flags(customer_relations: false)
stub_config(dependency_proxy: { enabled: false })
stub_config(registry: { enabled: false })
stub_group_wikis(false)
@@ -40,6 +41,22 @@ RSpec.describe 'Group navbar' do
it_behaves_like 'verified navigation bar'
end
+ context 'when customer_relations feature flag is enabled' do
+ before do
+ stub_feature_flags(customer_relations: true)
+
+ if Gitlab.ee?
+ insert_customer_relations_nav(_('Analytics'))
+ else
+ insert_customer_relations_nav(_('Packages & Registries'))
+ end
+
+ visit group_path(group)
+ end
+
+ it_behaves_like 'verified navigation bar'
+ end
+
context 'when dependency proxy is available' do
before do
stub_config(dependency_proxy: { enabled: true })
diff --git a/spec/features/groups/packages_spec.rb b/spec/features/groups/packages_spec.rb
index 0dfc7180187..3c2ade6b274 100644
--- a/spec/features/groups/packages_spec.rb
+++ b/spec/features/groups/packages_spec.rb
@@ -28,10 +28,6 @@ RSpec.describe 'Group Packages' do
context 'when feature is available', :js do
before do
- # we are simply setting the featrure flag to false because the new UI has nothing to test yet
- # when the refactor is complete or almost complete we will turn on the feature tests
- # see https://gitlab.com/gitlab-org/gitlab/-/issues/330846 for status of this work
- stub_feature_flags(package_list_apollo: false)
visit_group_packages
end
diff --git a/spec/features/groups/settings/manage_applications_spec.rb b/spec/features/groups/settings/manage_applications_spec.rb
index 5f84f61678d..277471cb304 100644
--- a/spec/features/groups/settings/manage_applications_spec.rb
+++ b/spec/features/groups/settings/manage_applications_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe 'User manages applications' do
let_it_be(:group) { create(:group) }
let_it_be(:user) { create(:user) }
let_it_be(:new_application_path) { group_settings_applications_path(group) }
+ let_it_be(:index_path) { group_settings_applications_path(group) }
before do
group.add_owner(user)
diff --git a/spec/features/incidents/user_creates_new_incident_spec.rb b/spec/features/incidents/user_creates_new_incident_spec.rb
index 99a137b5852..685f6ab791a 100644
--- a/spec/features/incidents/user_creates_new_incident_spec.rb
+++ b/spec/features/incidents/user_creates_new_incident_spec.rb
@@ -4,52 +4,49 @@ require 'spec_helper'
RSpec.describe 'Incident Management index', :js do
let_it_be(:project) { create(:project) }
- let_it_be(:developer) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
let_it_be(:guest) { create(:user) }
let_it_be(:incident) { create(:incident, project: project) }
before_all do
- project.add_developer(developer)
+ project.add_reporter(reporter)
project.add_guest(guest)
end
- shared_examples 'create incident form' do
- it 'shows the create new issue button' do
- expect(page).to have_selector('.create-incident-button')
- end
+ before do
+ sign_in(user)
- it 'when clicked shows the create issue page with the Incident type pre-selected' do
- find('.create-incident-button').click
- wait_for_all_requests
+ visit project_incidents_path(project)
+ wait_for_all_requests
+ end
- expect(page).to have_selector('.dropdown-menu-toggle')
- expect(page).to have_selector('.js-issuable-type-filter-dropdown-wrap')
+ describe 'incident list is visited' do
+ context 'by reporter' do
+ let(:user) { reporter }
- page.within('.js-issuable-type-filter-dropdown-wrap') do
- expect(page).to have_content('Incident')
+ it 'shows the create new incident button' do
+ expect(page).to have_selector('.create-incident-button')
end
- end
- end
- context 'when a developer displays the incident list' do
- before do
- sign_in(developer)
+ it 'when clicked shows the create issue page with the Incident type pre-selected' do
+ find('.create-incident-button').click
+ wait_for_all_requests
- visit project_incidents_path(project)
- wait_for_all_requests
- end
+ expect(page).to have_selector('.dropdown-menu-toggle')
+ expect(page).to have_selector('.js-issuable-type-filter-dropdown-wrap')
- it_behaves_like 'create incident form'
+ page.within('.js-issuable-type-filter-dropdown-wrap') do
+ expect(page).to have_content('Incident')
+ end
+ end
+ end
end
- context 'when a guest displays the incident list' do
- before do
- sign_in(guest)
+ context 'by guest' do
+ let(:user) { guest }
- visit project_incidents_path(project)
- wait_for_all_requests
+ it 'does not show new incident button' do
+ expect(page).not_to have_selector('.create-incident-button')
end
-
- it_behaves_like 'create incident form'
end
end
diff --git a/spec/features/incidents/user_views_incident_spec.rb b/spec/features/incidents/user_views_incident_spec.rb
index 244b66f7a9a..fe54f7708c9 100644
--- a/spec/features/incidents/user_views_incident_spec.rb
+++ b/spec/features/incidents/user_views_incident_spec.rb
@@ -22,12 +22,30 @@ RSpec.describe "User views incident" do
it_behaves_like 'page meta description', ' Description header Lorem ipsum dolor sit amet'
- it 'shows the merge request and incident actions', :js, :aggregate_failures do
- click_button 'Incident actions'
+ describe 'user actions' do
+ it 'shows the merge request and incident actions', :js, :aggregate_failures do
+ click_button 'Incident actions'
- expect(page).to have_link('New incident', href: new_project_issue_path(project, { issuable_template: 'incident', issue: { issue_type: 'incident', description: "Related to \##{incident.iid}.\n\n" } }))
- expect(page).to have_button('Create merge request')
- expect(page).to have_button('Close incident')
+ expect(page).to have_link('New incident', href: new_project_issue_path(project, { issuable_template: 'incident', issue: { issue_type: 'incident', description: "Related to \##{incident.iid}.\n\n" } }))
+ expect(page).to have_button('Create merge request')
+ expect(page).to have_button('Close incident')
+ end
+
+ context 'when user is a guest' do
+ before do
+ project.add_guest(user)
+
+ login_as(user)
+
+ visit(project_issues_incident_path(project, incident))
+ end
+
+ it 'does not show the incident action', :js, :aggregate_failures do
+ click_button 'Incident actions'
+
+ expect(page).not_to have_link('New incident')
+ end
+ end
end
context 'when the project is archived' do
diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb
index 87fb8955dcc..f9ab780d2d6 100644
--- a/spec/features/invites_spec.rb
+++ b/spec/features/invites_spec.rb
@@ -103,6 +103,20 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do
expect(page).to have_content('You are already a member of this group.')
end
end
+
+ context 'when email case doesnt match', :js do
+ let(:invite_email) { 'User@example.com' }
+ let(:user) { create(:user, email: 'user@example.com') }
+
+ before do
+ sign_in(user)
+ visit invite_path(group_invite.raw_invite_token)
+ end
+
+ it 'accepts invite' do
+ expect(page).to have_content('You have been granted Developer access to group Owned.')
+ end
+ end
end
context 'when declining the invitation from invitation reminder email' do
diff --git a/spec/features/issuables/markdown_references/internal_references_spec.rb b/spec/features/issuables/markdown_references/internal_references_spec.rb
index 07d4271eed7..2dcabb38b8f 100644
--- a/spec/features/issuables/markdown_references/internal_references_spec.rb
+++ b/spec/features/issuables/markdown_references/internal_references_spec.rb
@@ -53,9 +53,7 @@ RSpec.describe "Internal references", :js do
end
it "doesn't show any references", quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/257832' do
- page.within(".issue-details") do
- expect(page).not_to have_content("#merge-requests .merge-requests-title")
- end
+ expect(page).not_to have_text 'Related merge requests'
end
end
@@ -65,12 +63,9 @@ RSpec.describe "Internal references", :js do
end
it "shows references", :sidekiq_might_not_need_inline do
- page.within("#merge-requests .merge-requests-title") do
- expect(page).to have_content("Related merge requests")
- expect(page).to have_css(".mr-count-badge")
- end
+ expect(page).to have_text 'Related merge requests 1'
- page.within("#merge-requests ul") do
+ page.within('.related-items-list') do
expect(page).to have_content(private_project_merge_request.title)
expect(page).to have_css(".ic-issue-open-m")
end
@@ -122,9 +117,7 @@ RSpec.describe "Internal references", :js do
end
it "doesn't show any references", quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/257832' do
- page.within(".merge-request-details") do
- expect(page).not_to have_content("#merge-requests .merge-requests-title")
- end
+ expect(page).not_to have_text 'Related merge requests'
end
end
diff --git a/spec/features/issue_rebalancing_spec.rb b/spec/features/issue_rebalancing_spec.rb
new file mode 100644
index 00000000000..978768270ec
--- /dev/null
+++ b/spec/features/issue_rebalancing_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Issue rebalancing' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:user) { create(:user) }
+
+ let(:alert_message_regex) { /Issues are being rebalanced at the moment/ }
+
+ before_all do
+ create(:issue, project: project)
+
+ group.add_developer(user)
+ end
+
+ context 'when issue rebalancing is in progress' do
+ before do
+ sign_in(user)
+
+ stub_feature_flags(block_issue_repositioning: true)
+ end
+
+ it 'shows an alert in project boards' do
+ board = create(:board, project: project)
+
+ visit project_board_path(project, board)
+
+ expect(page).to have_selector('.gl-alert-info', text: alert_message_regex, count: 1)
+ end
+
+ it 'shows an alert in group boards' do
+ board = create(:board, group: group)
+
+ visit group_board_path(group, board)
+
+ expect(page).to have_selector('.gl-alert-info', text: alert_message_regex, count: 1)
+ end
+
+ it 'shows an alert in project issues list with manual sort' do
+ visit project_issues_path(project, sort: 'relative_position')
+
+ expect(page).to have_selector('.gl-alert-info', text: alert_message_regex, count: 1)
+ end
+
+ it 'shows an alert in group issues list with manual sort' do
+ visit issues_group_path(group, sort: 'relative_position')
+
+ expect(page).to have_selector('.gl-alert-info', text: alert_message_regex, count: 1)
+ end
+
+ it 'does not show an alert in project issues list with other sorts' do
+ visit project_issues_path(project, sort: 'created_date')
+
+ expect(page).not_to have_selector('.gl-alert-info', text: alert_message_regex)
+ end
+
+ it 'does not show an alert in group issues list with other sorts' do
+ visit issues_group_path(group, sort: 'created_date')
+
+ expect(page).not_to have_selector('.gl-alert-info', text: alert_message_regex)
+ end
+ end
+end
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index 4bad67acc87..b26f65316c5 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -4,25 +4,29 @@ require 'spec_helper'
RSpec.describe 'New/edit issue', :js do
include ActionView::Helpers::JavaScriptHelper
- include FormHelper
let_it_be(:project) { create(:project) }
- let_it_be(:user) { create(:user)}
- let_it_be(:user2) { create(:user)}
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user2) { create(:user) }
let_it_be(:milestone) { create(:milestone, project: project) }
let_it_be(:label) { create(:label, project: project) }
let_it_be(:label2) { create(:label, project: project) }
let_it_be(:issue) { create(:issue, project: project, assignees: [user], milestone: milestone) }
- before do
- stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
+ let(:current_user) { user }
+ before_all do
project.add_maintainer(user)
project.add_maintainer(user2)
- sign_in(user)
end
- context 'new issue' do
+ before do
+ stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
+
+ sign_in(current_user)
+ end
+
+ describe 'new issue' do
before do
visit new_project_issue_path(project)
end
@@ -235,29 +239,61 @@ RSpec.describe 'New/edit issue', :js do
end
describe 'displays issue type options in the dropdown' do
+ shared_examples 'type option is visible' do |label:, identifier:|
+ it "shows #{identifier} option", :aggregate_failures do
+ page.within('[data-testid="issue-type-select-dropdown"]') do
+ expect(page).to have_selector(%([data-testid="issue-type-#{identifier}-icon"]))
+ expect(page).to have_content(label)
+ end
+ end
+ end
+
+ shared_examples 'type option is missing' do |label:, identifier:|
+ it "does not show #{identifier} option", :aggregate_failures do
+ page.within('[data-testid="issue-type-select-dropdown"]') do
+ expect(page).not_to have_selector(%([data-testid="issue-type-#{identifier}-icon"]))
+ expect(page).not_to have_content(label)
+ end
+ end
+ end
+
before do
page.within('.issue-form') do
click_button 'Issue'
end
end
- it 'correctly displays the Issue type option with an icon', :aggregate_failures do
- page.within('[data-testid="issue-type-select-dropdown"]') do
- expect(page).to have_selector('[data-testid="issue-type-issue-icon"]')
- expect(page).to have_content('Issue')
+ context 'when user is guest' do
+ let_it_be(:guest) { create(:user) }
+
+ let(:current_user) { guest }
+
+ before_all do
+ project.add_guest(guest)
end
+
+ it_behaves_like 'type option is visible', label: 'Issue', identifier: :issue
+ it_behaves_like 'type option is missing', label: 'Incident', identifier: :incident
end
- it 'correctly displays the Incident type option with an icon', :aggregate_failures do
- page.within('[data-testid="issue-type-select-dropdown"]') do
- expect(page).to have_selector('[data-testid="issue-type-incident-icon"]')
- expect(page).to have_content('Incident')
+ context 'when user is reporter' do
+ let_it_be(:reporter) { create(:user) }
+
+ let(:current_user) { reporter }
+
+ before_all do
+ project.add_reporter(reporter)
end
+
+ it_behaves_like 'type option is visible', label: 'Issue', identifier: :issue
+ it_behaves_like 'type option is visible', label: 'Incident', identifier: :incident
end
end
describe 'milestone' do
- let!(:milestone) { create(:milestone, title: '">&lt;img src=x onerror=alert(document.domain)&gt;', project: project) }
+ let!(:milestone) do
+ create(:milestone, title: '">&lt;img src=x onerror=alert(document.domain)&gt;', project: project)
+ end
it 'escapes milestone' do
click_button 'Milestone'
@@ -274,7 +310,7 @@ RSpec.describe 'New/edit issue', :js do
end
end
- context 'edit issue' do
+ describe 'edit issue' do
before do
visit edit_project_issue_path(project, issue)
end
@@ -329,7 +365,7 @@ RSpec.describe 'New/edit issue', :js do
end
end
- context 'inline edit' do
+ describe 'inline edit' do
before do
visit project_issue_path(project, issue)
end
diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb
index 531c3634b5e..b37c8e9d1cf 100644
--- a/spec/features/issues/issue_detail_spec.rb
+++ b/spec/features/issues/issue_detail_spec.rb
@@ -3,8 +3,9 @@
require 'spec_helper'
RSpec.describe 'Issue Detail', :js do
+ let_it_be_with_refind(:project) { create(:project, :public) }
+
let(:user) { create(:user) }
- let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project, author: user) }
let(:incident) { create(:incident, project: project, author: user) }
@@ -90,7 +91,13 @@ RSpec.describe 'Issue Detail', :js do
end
describe 'user updates `issue_type` via the issue type dropdown' do
- context 'when an issue `issue_type` is edited by a signed in user' do
+ let_it_be(:reporter) { create(:user) }
+
+ before_all do
+ project.add_reporter(reporter)
+ end
+
+ describe 'when an issue `issue_type` is edited' do
before do
sign_in(user)
@@ -98,18 +105,33 @@ RSpec.describe 'Issue Detail', :js do
wait_for_requests
end
- it 'routes the user to the incident details page when the `issue_type` is set to incident' do
- open_issue_edit_form
+ context 'by non-member author' do
+ it 'cannot see Incident option' do
+ open_issue_edit_form
+
+ page.within('[data-testid="issuable-form"]') do
+ expect(page).to have_content('Issue')
+ expect(page).not_to have_content('Incident')
+ end
+ end
+ end
+
+ context 'by reporter' do
+ let(:user) { reporter }
- page.within('[data-testid="issuable-form"]') do
- update_type_select('Issue', 'Incident')
+ it 'routes the user to the incident details page when the `issue_type` is set to incident' do
+ open_issue_edit_form
- expect(page).to have_current_path(project_issues_incident_path(project, issue))
+ page.within('[data-testid="issuable-form"]') do
+ update_type_select('Issue', 'Incident')
+
+ expect(page).to have_current_path(project_issues_incident_path(project, issue))
+ end
end
end
end
- context 'when an incident `issue_type` is edited by a signed in user' do
+ describe 'when an incident `issue_type` is edited' do
before do
sign_in(user)
@@ -117,13 +139,29 @@ RSpec.describe 'Issue Detail', :js do
wait_for_requests
end
- it 'routes the user to the issue details page when the `issue_type` is set to issue' do
- open_issue_edit_form
+ context 'by non-member author' do
+ it 'routes the user to the issue details page when the `issue_type` is set to issue' do
+ open_issue_edit_form
+
+ page.within('[data-testid="issuable-form"]') do
+ update_type_select('Incident', 'Issue')
+
+ expect(page).to have_current_path(project_issue_path(project, incident))
+ end
+ end
+ end
+
+ context 'by reporter' do
+ let(:user) { reporter }
+
+ it 'routes the user to the issue details page when the `issue_type` is set to issue' do
+ open_issue_edit_form
- page.within('[data-testid="issuable-form"]') do
- update_type_select('Incident', 'Issue')
+ page.within('[data-testid="issuable-form"]') do
+ update_type_select('Incident', 'Issue')
- expect(page).to have_current_path(project_issue_path(project, incident))
+ expect(page).to have_current_path(project_issue_path(project, incident))
+ end
end
end
end
diff --git a/spec/features/issues/user_creates_issue_spec.rb b/spec/features/issues/user_creates_issue_spec.rb
index f46aa5c21b6..37e324e6ded 100644
--- a/spec/features/issues/user_creates_issue_spec.rb
+++ b/spec/features/issues/user_creates_issue_spec.rb
@@ -171,7 +171,7 @@ RSpec.describe "User creates issue" do
end
context 'form create handles issue creation by default' do
- let(:project) { create(:project) }
+ let_it_be(:project) { create(:project) }
before do
visit new_project_issue_path(project)
@@ -187,30 +187,22 @@ RSpec.describe "User creates issue" do
end
context 'form create handles incident creation' do
- let(:project) { create(:project) }
+ let_it_be(:project) { create(:project) }
before do
visit new_project_issue_path(project, { issuable_template: 'incident', issue: { issue_type: 'incident' } })
end
- it 'pre-fills the issue type dropdown with incident type' do
- expect(find('.js-issuable-type-filter-dropdown-wrap .dropdown-toggle-text')).to have_content('Incident')
- end
-
- it 'hides the epic select' do
- expect(page).not_to have_selector('.epic-dropdown-container')
+ it 'does not pre-fill the issue type dropdown with incident type' do
+ expect(find('.js-issuable-type-filter-dropdown-wrap .dropdown-toggle-text')).not_to have_content('Incident')
end
it 'shows the milestone select' do
expect(page).to have_selector('.qa-issuable-milestone-dropdown') # rubocop:disable QA/SelectorUsage
end
- it 'hides the weight input' do
- expect(page).not_to have_selector('.qa-issuable-weight-input') # rubocop:disable QA/SelectorUsage
- end
-
- it 'shows the incident help text' do
- expect(page).to have_text('A modified issue to guide the resolution of incidents.')
+ it 'hides the incident help text' do
+ expect(page).not_to have_text('A modified issue to guide the resolution of incidents.')
end
end
@@ -242,6 +234,44 @@ RSpec.describe "User creates issue" do
end
end
+ context 'when signed in as reporter', :js do
+ let_it_be(:project) { create(:project) }
+
+ before_all do
+ project.add_reporter(user)
+ end
+
+ before do
+ sign_in(user)
+ end
+
+ context 'form create handles incident creation' do
+ before do
+ visit new_project_issue_path(project, { issuable_template: 'incident', issue: { issue_type: 'incident' } })
+ end
+
+ it 'pre-fills the issue type dropdown with incident type' do
+ expect(find('.js-issuable-type-filter-dropdown-wrap .dropdown-toggle-text')).to have_content('Incident')
+ end
+
+ it 'hides the epic select' do
+ expect(page).not_to have_selector('.epic-dropdown-container')
+ end
+
+ it 'shows the milestone select' do
+ expect(page).to have_selector('.qa-issuable-milestone-dropdown') # rubocop:disable QA/SelectorUsage
+ end
+
+ it 'hides the weight input' do
+ expect(page).not_to have_selector('.qa-issuable-weight-input') # rubocop:disable QA/SelectorUsage
+ end
+
+ it 'shows the incident help text' do
+ expect(page).to have_text('A modified issue to guide the resolution of incidents.')
+ end
+ end
+ end
+
context "when signed in as user with special characters in their name" do
let(:user_special) { create(:user, name: "Jon O'Shea") }
diff --git a/spec/features/issues/user_edits_issue_spec.rb b/spec/features/issues/user_edits_issue_spec.rb
index 63c36a20adc..76cec2502e3 100644
--- a/spec/features/issues/user_edits_issue_spec.rb
+++ b/spec/features/issues/user_edits_issue_spec.rb
@@ -146,8 +146,7 @@ RSpec.describe "Issues > User edits issue", :js do
fill_in 'Comment', with: '/label ~syzygy'
click_button 'Comment'
-
- wait_for_requests
+ expect(page).to have_text('added syzygy label just now')
page.within '.block.labels' do
# Remove `verisimilitude` label
@@ -155,8 +154,6 @@ RSpec.describe "Issues > User edits issue", :js do
click_button
end
- wait_for_requests
-
expect(page).to have_text('syzygy')
expect(page).not_to have_text('verisimilitude')
end
diff --git a/spec/features/issues/user_toggles_subscription_spec.rb b/spec/features/issues/user_toggles_subscription_spec.rb
index 9809bb34d26..541bbc8a8e7 100644
--- a/spec/features/issues/user_toggles_subscription_spec.rb
+++ b/spec/features/issues/user_toggles_subscription_spec.rb
@@ -45,7 +45,7 @@ RSpec.describe "User toggles subscription", :js do
it 'is disabled' do
expect(page).to have_content('Disabled by project owner')
- expect(page).to have_button('Notifications', class: 'is-disabled')
+ expect(page).to have_selector('[data-testid="subscription-toggle"]', class: 'is-disabled')
end
end
end
diff --git a/spec/features/issues/user_uses_quick_actions_spec.rb b/spec/features/issues/user_uses_quick_actions_spec.rb
index d88b816b186..c6d743ed38f 100644
--- a/spec/features/issues/user_uses_quick_actions_spec.rb
+++ b/spec/features/issues/user_uses_quick_actions_spec.rb
@@ -44,5 +44,6 @@ RSpec.describe 'Issues > User uses quick actions', :js do
it_behaves_like 'move quick action'
it_behaves_like 'zoom quick actions'
it_behaves_like 'clone quick action'
+ it_behaves_like 'promote_to_incident quick action'
end
end
diff --git a/spec/features/jira_connect/subscriptions_spec.rb b/spec/features/jira_connect/subscriptions_spec.rb
index 9be6b7c67ee..e1589ba997e 100644
--- a/spec/features/jira_connect/subscriptions_spec.rb
+++ b/spec/features/jira_connect/subscriptions_spec.rb
@@ -40,8 +40,8 @@ RSpec.describe 'Subscriptions Content Security Policy' do
visit jira_connect_subscriptions_path(jwt: jwt)
is_expected.to include("frame-ancestors 'self' https://*.atlassian.net")
- is_expected.to include("script-src 'self' https://some-cdn.test https://connect-cdn.atl-paas.net https://unpkg.com/jquery@3.3.1/")
- is_expected.to include("style-src 'self' https://some-cdn.test 'unsafe-inline' https://unpkg.com/@atlaskit/")
+ is_expected.to include("script-src 'self' https://some-cdn.test https://connect-cdn.atl-paas.net")
+ is_expected.to include("style-src 'self' https://some-cdn.test 'unsafe-inline'")
end
end
end
diff --git a/spec/features/merge_request/user_approves_spec.rb b/spec/features/merge_request/user_approves_spec.rb
index f401dd598f3..4f7bcb58551 100644
--- a/spec/features/merge_request/user_approves_spec.rb
+++ b/spec/features/merge_request/user_approves_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe 'Merge request > User approves', :js do
it 'approves merge request' do
click_approval_button('Approve')
- expect(page).to have_content('Merge request approved')
+ expect(page).to have_content('Approved by you')
verify_approvals_count_on_index!
diff --git a/spec/features/merge_request/user_assigns_themselves_spec.rb b/spec/features/merge_request/user_assigns_themselves_spec.rb
index 04d401683bf..fc925781a3b 100644
--- a/spec/features/merge_request/user_assigns_themselves_spec.rb
+++ b/spec/features/merge_request/user_assigns_themselves_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe 'Merge request > User assigns themselves' do
visit project_merge_request_path(project, merge_request)
end
- it 'updates related issues', :js do
+ it 'updates related issues', :js, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/343006' do
click_link 'Assign yourself to these issues'
expect(page).to have_content '2 issues have been assigned to you'
diff --git a/spec/features/merge_request/user_comments_on_diff_spec.rb b/spec/features/merge_request/user_comments_on_diff_spec.rb
index 54c3fe738d2..f9b554c5ed2 100644
--- a/spec/features/merge_request/user_comments_on_diff_spec.rb
+++ b/spec/features/merge_request/user_comments_on_diff_spec.rb
@@ -14,6 +14,7 @@ RSpec.describe 'User comments on a diff', :js do
let(:user) { create(:user) }
before do
+ stub_feature_flags(bootstrap_confirmation_modals: false)
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/merge_request/user_customizes_merge_commit_message_spec.rb b/spec/features/merge_request/user_customizes_merge_commit_message_spec.rb
index 1d3d76d3486..06795344c5c 100644
--- a/spec/features/merge_request/user_customizes_merge_commit_message_spec.rb
+++ b/spec/features/merge_request/user_customizes_merge_commit_message_spec.rb
@@ -26,33 +26,27 @@ RSpec.describe 'Merge request < User customizes merge commit message', :js do
].join("\n\n")
end
- let(:message_with_description) do
- [
- "Merge branch 'feature' into 'master'",
- merge_request.title,
- merge_request.description,
- "See merge request #{merge_request.to_reference(full: true)}"
- ].join("\n\n")
- end
-
before do
project.add_maintainer(user)
sign_in(user)
visit project_merge_request_path(project, merge_request)
end
- it 'toggles commit message between message with description and without description' do
+ it 'has commit message without description' do
expect(page).not_to have_selector('#merge-message-edit')
first('.js-mr-widget-commits-count').click
expect(textbox).to be_visible
expect(textbox.value).to eq(default_message)
+ end
- check('Include merge request description')
-
- expect(textbox.value).to eq(message_with_description)
-
- uncheck('Include merge request description')
+ context 'when target project has merge commit template set' do
+ let(:project) { create(:project, :public, :repository, merge_commit_template: '%{title}') }
- expect(textbox.value).to eq(default_message)
+ it 'uses merge commit template' do
+ expect(page).not_to have_selector('#merge-message-edit')
+ first('.js-mr-widget-commits-count').click
+ expect(textbox).to be_visible
+ expect(textbox.value).to eq(merge_request.title)
+ end
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 af5ba14e310..9057b96bff0 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
@@ -36,7 +36,7 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do
click_button "Merge when pipeline succeeds"
expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds"
- expect(page).to have_content "The source branch will not be deleted"
+ expect(page).to have_content "Does not delete the source branch"
expect(page).to have_selector ".js-cancel-auto-merge"
visit project_merge_request_path(project, merge_request) # Needed to refresh the page
expect(page).to have_content /enabled an automatic merge when the pipeline for \h{8} succeeds/i
@@ -126,7 +126,7 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do
it 'allows to delete source branch' do
click_button "Delete source branch"
- expect(page).to have_content "The source branch will be deleted"
+ expect(page).to have_content "Deletes the source branch"
end
context 'when pipeline succeeds' do
diff --git a/spec/features/merge_request/user_posts_diff_notes_spec.rb b/spec/features/merge_request/user_posts_diff_notes_spec.rb
index c339a7d9976..79e46e69157 100644
--- a/spec/features/merge_request/user_posts_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_posts_diff_notes_spec.rb
@@ -18,6 +18,7 @@ RSpec.describe 'Merge request > User posts diff notes', :js do
project.add_developer(user)
sign_in(user)
+ stub_feature_flags(bootstrap_confirmation_modals: false)
end
context 'when hovering over a parallel view diff file' do
@@ -237,8 +238,10 @@ RSpec.describe 'Merge request > User posts diff notes', :js do
def should_allow_dismissing_a_comment(line_holder, diff_side = nil)
write_comment_on_line(line_holder, diff_side)
- accept_confirm do
- find('.js-close-discussion-note-form').click
+ find('.js-close-discussion-note-form').click
+
+ page.within('.modal') do
+ click_button 'OK'
end
assert_comment_dismissal(line_holder)
diff --git a/spec/features/merge_request/user_posts_notes_spec.rb b/spec/features/merge_request/user_posts_notes_spec.rb
index 83d9388914b..0416474218f 100644
--- a/spec/features/merge_request/user_posts_notes_spec.rb
+++ b/spec/features/merge_request/user_posts_notes_spec.rb
@@ -18,8 +18,10 @@ RSpec.describe 'Merge request > User posts notes', :js do
end
before do
+ stub_feature_flags(bootstrap_confirmation_modals: false)
project.add_maintainer(user)
sign_in(user)
+
visit project_merge_request_path(project, merge_request)
end
diff --git a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
index 90cdc28d1bd..64cd5aa2bb1 100644
--- a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
@@ -79,6 +79,7 @@ RSpec.describe 'Merge request > User sees avatars on diff notes', :js do
%w(parallel).each do |view|
context "#{view} view" do
before do
+ stub_feature_flags(bootstrap_confirmation_modals: false)
visit diffs_project_merge_request_path(project, merge_request, view: view)
wait_for_requests
diff --git a/spec/features/merge_request/user_sees_deployment_widget_spec.rb b/spec/features/merge_request/user_sees_deployment_widget_spec.rb
index 873cc0a89c6..345404cc28f 100644
--- a/spec/features/merge_request/user_sees_deployment_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_deployment_widget_spec.rb
@@ -110,6 +110,7 @@ RSpec.describe 'Merge request > User sees deployment widget', :js do
let(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
before do
+ stub_feature_flags(bootstrap_confirmation_modals: false)
build.success!
deployment.update!(on_stop: manual.name)
visit project_merge_request_path(project, merge_request)
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 f74b097ab3e..0117cf01e53 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -426,7 +426,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
it 'user cannot remove source branch', :sidekiq_might_not_need_inline do
expect(page).not_to have_field('remove-source-branch-input')
- expect(page).to have_content('The source branch will be deleted')
+ expect(page).to have_content('Deletes the source branch')
end
end
diff --git a/spec/features/merge_request/user_sees_suggest_pipeline_spec.rb b/spec/features/merge_request/user_sees_suggest_pipeline_spec.rb
index 3893a9cdf28..2191849edd9 100644
--- a/spec/features/merge_request/user_sees_suggest_pipeline_spec.rb
+++ b/spec/features/merge_request/user_sees_suggest_pipeline_spec.rb
@@ -16,7 +16,8 @@ RSpec.describe 'Merge request > User sees suggest pipeline', :js do
end
it 'shows the suggest pipeline widget and then allows dismissal correctly' do
- expect(page).to have_content('Are you adding technical debt or code vulnerabilities?')
+ content = 'GitLab CI/CD can automatically build, test, and deploy your application'
+ expect(page).to have_content(content)
page.within '.mr-pipeline-suggest' do
find('[data-testid="close"]').click
@@ -24,17 +25,16 @@ RSpec.describe 'Merge request > User sees suggest pipeline', :js do
wait_for_requests
- expect(page).not_to have_content('Are you adding technical debt or code vulnerabilities?')
+ expect(page).not_to have_content(content)
# Reload so we know the user callout was registered
visit page.current_url
- expect(page).not_to have_content('Are you adding technical debt or code vulnerabilities?')
+ expect(page).not_to have_content(content)
end
- it 'runs tour from start to finish ensuring all nudges are executed' do
- # nudge 1
- expect(page).to have_content('Are you adding technical debt or code vulnerabilities?')
+ it 'takes the user to the pipeline editor with a pre-filled CI config file form' do
+ expect(page).to have_content('GitLab CI/CD can automatically build, test, and deploy your application')
page.within '.mr-pipeline-suggest' do
find('[data-testid="ok"]').click
@@ -42,30 +42,14 @@ RSpec.describe 'Merge request > User sees suggest pipeline', :js do
wait_for_requests
- # nudge 2
- expect(page).to have_content('Choose Code Quality to add a pipeline that tests the quality of your code.')
+ # Drawer is open
+ expect(page).to have_content('This template creates a simple test pipeline. To use it:')
- find('.js-gitlab-ci-yml-selector').click
+ # Editor shows template
+ expect(page).to have_content('This file is a template, and might need editing before it works on your project.')
- wait_for_requests
-
- within '.gitlab-ci-yml-selector' do
- find('.dropdown-input-field').set('Jekyll')
- find('.dropdown-content li', text: 'Jekyll').click
- end
-
- wait_for_requests
-
- expect(page).not_to have_content('Choose Code Quality to add a pipeline that tests the quality of your code.')
- # nudge 3
- expect(page).to have_content('The template is ready!')
-
- find('#commit-changes').click
-
- wait_for_requests
-
- # nudge 4
- expect(page).to have_content("That's it, well done!")
+ # Commit form is shown
+ expect(page).to have_button('Commit changes')
end
context 'when feature setting is disabled' do
diff --git a/spec/features/oauth_login_spec.rb b/spec/features/oauth_login_spec.rb
index 3402bda5a41..0ea14bc00a5 100644
--- a/spec/features/oauth_login_spec.rb
+++ b/spec/features/oauth_login_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe 'OAuth Login', :js, :allow_forgery_protection do
end
providers = [:github, :twitter, :bitbucket, :gitlab, :google_oauth2,
- :facebook, :cas3, :auth0, :authentiq, :salesforce]
+ :facebook, :cas3, :auth0, :authentiq, :salesforce, :dingtalk]
around do |example|
with_omniauth_full_host { example.run }
diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb
index 9a261c6d9c8..7d935298f38 100644
--- a/spec/features/profile_spec.rb
+++ b/spec/features/profile_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe 'Profile account page', :js do
let(:user) { create(:user) }
before do
+ stub_feature_flags(bootstrap_confirmation_modals: false)
sign_in(user)
end
@@ -80,6 +81,7 @@ RSpec.describe 'Profile account page', :js do
describe 'when I reset incoming email token' do
before do
allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
+ stub_feature_flags(bootstrap_confirmation_modals: false)
visit profile_personal_access_tokens_path
end
diff --git a/spec/features/profiles/active_sessions_spec.rb b/spec/features/profiles/active_sessions_spec.rb
index fd64704b7c8..a515c7b1c1f 100644
--- a/spec/features/profiles/active_sessions_spec.rb
+++ b/spec/features/profiles/active_sessions_spec.rb
@@ -11,6 +11,10 @@ RSpec.describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do
let(:admin) { create(:admin) }
+ before do
+ stub_feature_flags(bootstrap_confirmation_modals: false)
+ end
+
it 'user sees their active sessions' do
travel_to(Time.zone.parse('2018-03-12 09:06')) do
Capybara::Session.new(:session1)
diff --git a/spec/features/profiles/emails_spec.rb b/spec/features/profiles/emails_spec.rb
index 6b6f628e2d5..8f05de60be9 100644
--- a/spec/features/profiles/emails_spec.rb
+++ b/spec/features/profiles/emails_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'Profile > Emails' do
let(:user) { create(:user) }
+ let(:other_user) { create(:user) }
before do
sign_in(user)
@@ -23,15 +24,25 @@ RSpec.describe 'Profile > Emails' do
expect(page).to have_content('Resend confirmation email')
end
- it 'does not add a duplicate email' do
- fill_in('Email', with: user.email)
+ it 'does not add an email that is the primary email of another user' do
+ fill_in('Email', with: other_user.email)
click_button('Add email address')
- email = user.emails.find_by(email: user.email)
+ email = user.emails.find_by(email: other_user.email)
expect(email).to be_nil
expect(page).to have_content('Email has already been taken')
end
+ it 'adds an email that is the primary email of the same user' do
+ fill_in('Email', with: user.email)
+ click_button('Add email address')
+
+ email = user.emails.find_by(email: user.email)
+ expect(email).to be_present
+ expect(page).to have_content("#{user.email} Verified")
+ expect(page).not_to have_content("#{user.email} Unverified")
+ end
+
it 'does not add an invalid email' do
fill_in('Email', with: 'test.@example.com')
click_button('Add email address')
diff --git a/spec/features/profiles/oauth_applications_spec.rb b/spec/features/profiles/oauth_applications_spec.rb
index 2735f601307..6827dff5434 100644
--- a/spec/features/profiles/oauth_applications_spec.rb
+++ b/spec/features/profiles/oauth_applications_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe 'Profile > Applications' do
let(:application) { create(:oauth_application, owner: user) }
before do
+ stub_feature_flags(bootstrap_confirmation_modals: false)
sign_in(user)
end
diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb
index 8f44299b18f..74505633cae 100644
--- a/spec/features/profiles/personal_access_tokens_spec.rb
+++ b/spec/features/profiles/personal_access_tokens_spec.rb
@@ -34,6 +34,7 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do
end
before do
+ stub_feature_flags(bootstrap_confirmation_modals: false)
sign_in(user)
end
diff --git a/spec/features/profiles/two_factor_auths_spec.rb b/spec/features/profiles/two_factor_auths_spec.rb
index 3f5789e119a..a9256a73d7b 100644
--- a/spec/features/profiles/two_factor_auths_spec.rb
+++ b/spec/features/profiles/two_factor_auths_spec.rb
@@ -45,6 +45,19 @@ RSpec.describe 'Two factor auths' do
expect(page).to have_content('Status: Enabled')
end
end
+
+ context 'when invalid pin is provided' do
+ let_it_be(:user) { create(:omniauth_user) }
+
+ it 'renders a error alert with a link to the troubleshooting section' do
+ visit profile_two_factor_auth_path
+
+ fill_in 'pin_code', with: '123'
+ click_button 'Register with two-factor app'
+
+ expect(page).to have_link('Try the troubleshooting steps here.', href: help_page_path('user/profile/account/two_factor_authentication.md', anchor: 'troubleshooting'))
+ end
+ end
end
context 'when user has two-factor authentication enabled' do
@@ -57,7 +70,9 @@ RSpec.describe 'Two factor auths' do
click_button 'Disable two-factor authentication'
- page.accept_alert
+ page.within('[role="dialog"]') do
+ click_button 'Disable'
+ end
expect(page).to have_content('You must provide a valid current password')
@@ -65,7 +80,9 @@ RSpec.describe 'Two factor auths' do
click_button 'Disable two-factor authentication'
- page.accept_alert
+ page.within('[role="dialog"]') do
+ click_button 'Disable'
+ end
expect(page).to have_content('Two-factor authentication has been disabled successfully!')
expect(page).to have_content('Enable two-factor authentication')
@@ -95,7 +112,9 @@ RSpec.describe 'Two factor auths' do
click_button 'Disable two-factor authentication'
- page.accept_alert
+ page.within('[role="dialog"]') do
+ click_button 'Disable'
+ end
expect(page).to have_content('Two-factor authentication has been disabled successfully!')
expect(page).to have_content('Enable two-factor authentication')
diff --git a/spec/features/profiles/user_manages_applications_spec.rb b/spec/features/profiles/user_manages_applications_spec.rb
index c76ef2613fd..ea7a6b4b6ba 100644
--- a/spec/features/profiles/user_manages_applications_spec.rb
+++ b/spec/features/profiles/user_manages_applications_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe 'User manages applications' do
let_it_be(:user) { create(:user) }
let_it_be(:new_application_path) { applications_profile_path }
+ let_it_be(:index_path) { oauth_applications_path }
before do
sign_in(user)
diff --git a/spec/features/profiles/user_manages_emails_spec.rb b/spec/features/profiles/user_manages_emails_spec.rb
index 373c4f565f2..b037d5048aa 100644
--- a/spec/features/profiles/user_manages_emails_spec.rb
+++ b/spec/features/profiles/user_manages_emails_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'User manages emails' do
let(:user) { create(:user) }
+ let(:other_user) { create(:user) }
before do
sign_in(user)
@@ -11,7 +12,7 @@ RSpec.describe 'User manages emails' do
visit(profile_emails_path)
end
- it "shows user's emails" do
+ it "shows user's emails", :aggregate_failures do
expect(page).to have_content(user.email)
user.emails.each do |email|
@@ -19,7 +20,7 @@ RSpec.describe 'User manages emails' do
end
end
- it 'adds an email' do
+ it 'adds an email', :aggregate_failures do
fill_in('email_email', with: 'my@email.com')
click_button('Add')
@@ -34,21 +35,21 @@ RSpec.describe 'User manages emails' do
end
end
- it 'does not add a duplicate email' do
- fill_in('email_email', with: user.email)
+ it 'does not add an email that is the primary email of another user', :aggregate_failures do
+ fill_in('email_email', with: other_user.email)
click_button('Add')
- email = user.emails.find_by(email: user.email)
+ email = user.emails.find_by(email: other_user.email)
expect(email).to be_nil
- expect(page).to have_content(user.email)
+ expect(page).to have_content('Email has already been taken')
user.emails.each do |email|
expect(page).to have_content(email.email)
end
end
- it 'removes an email' do
+ it 'removes an email', :aggregate_failures do
fill_in('email_email', with: 'my@email.com')
click_button('Add')
diff --git a/spec/features/profiles/user_visits_profile_spec.rb b/spec/features/profiles/user_visits_profile_spec.rb
index 475fda5e7a1..273d52996d3 100644
--- a/spec/features/profiles/user_visits_profile_spec.rb
+++ b/spec/features/profiles/user_visits_profile_spec.rb
@@ -21,6 +21,14 @@ RSpec.describe 'User visits their profile' do
expect(page).to have_content "This information will appear on your profile"
end
+ it 'shows user readme' do
+ create(:project, :repository, :public, path: user.username, namespace: user.namespace)
+
+ visit(user_path(user))
+
+ expect(find('.file-content')).to have_content('testme')
+ end
+
context 'when user has groups' do
let(:group) do
create :group do |group|
diff --git a/spec/features/project_variables_spec.rb b/spec/features/project_variables_spec.rb
index 5139c724d82..cc59fea173b 100644
--- a/spec/features/project_variables_spec.rb
+++ b/spec/features/project_variables_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe 'Project variables', :js do
click_button('Add variable')
page.within('#add-ci-variable') do
- find('[data-qa-selector="ci_variable_key_field"] input').set('akey') # rubocop:disable QA/SelectorUsage
+ fill_in 'Key', with: 'akey'
find('#ci-variable-value').set('akey_value')
find('[data-testid="environment-scope"]').click
find('[data-testid="ci-environment-search"]').set('review/*')
diff --git a/spec/features/projects/branches/user_deletes_branch_spec.rb b/spec/features/projects/branches/user_deletes_branch_spec.rb
index 3b8f49accc5..8fc5c3d2e1b 100644
--- a/spec/features/projects/branches/user_deletes_branch_spec.rb
+++ b/spec/features/projects/branches/user_deletes_branch_spec.rb
@@ -35,6 +35,7 @@ RSpec.describe "User deletes branch", :js do
context 'when the feature flag :delete_branch_confirmation_modals is disabled' do
before do
+ stub_feature_flags(bootstrap_confirmation_modals: false)
stub_feature_flags(delete_branch_confirmation_modals: false)
end
diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb
index 0a79719f14a..2725c6a91be 100644
--- a/spec/features/projects/branches_spec.rb
+++ b/spec/features/projects/branches_spec.rb
@@ -179,6 +179,7 @@ RSpec.describe 'Branches' do
context 'when the delete_branch_confirmation_modals feature flag is disabled' do
it 'removes branch after confirmation', :js do
stub_feature_flags(delete_branch_confirmation_modals: false)
+ stub_feature_flags(bootstrap_confirmation_modals: false)
visit project_branches_filtered_path(project, state: 'all')
diff --git a/spec/features/projects/cluster_agents_spec.rb b/spec/features/projects/cluster_agents_spec.rb
new file mode 100644
index 00000000000..3ef710169f0
--- /dev/null
+++ b/spec/features/projects/cluster_agents_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'ClusterAgents', :js do
+ let_it_be(:token) { create(:cluster_agent_token, description: 'feature test token')}
+
+ let(:agent) { token.agent }
+ let(:project) { agent.project }
+ let(:user) { project.creator }
+
+ before do
+ gitlab_sign_in(user)
+ end
+
+ context 'when user does not have any agents and visits the index page' do
+ let(:empty_project) { create(:project) }
+
+ before do
+ empty_project.add_maintainer(user)
+ visit project_clusters_path(empty_project)
+ end
+
+ it 'displays empty state', :aggregate_failures do
+ expect(page).to have_content('Install new Agent')
+ expect(page).to have_selector('.empty-state')
+ end
+ end
+
+ context 'when user has an agent' do
+ context 'when visiting the index page' do
+ before do
+ visit project_clusters_path(project)
+ end
+
+ it 'displays a table with agent', :aggregate_failures do
+ expect(page).to have_content(agent.name)
+ expect(page).to have_selector('[data-testid="cluster-agent-list-table"] tbody tr', count: 1)
+ end
+ end
+
+ context 'when visiting the show page' do
+ before do
+ visit project_cluster_agent_path(project, agent.name)
+ end
+
+ it 'displays agent and token information', :aggregate_failures do
+ expect(page).to have_content(agent.name)
+ expect(page).to have_content(token.description)
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/clusters/eks_spec.rb b/spec/features/projects/clusters/eks_spec.rb
index 9f3f331cfab..09c10c0b3a9 100644
--- a/spec/features/projects/clusters/eks_spec.rb
+++ b/spec/features/projects/clusters/eks_spec.rb
@@ -19,7 +19,8 @@ RSpec.describe 'AWS EKS Cluster', :js do
before do
visit project_clusters_path(project)
- click_link 'Integrate with a cluster certificate'
+ click_link 'Certificate based'
+ click_link 'Connect with a certificate'
end
context 'when user creates a cluster on AWS EKS' do
diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb
index 21e587288f5..e1659cd2fbf 100644
--- a/spec/features/projects/clusters/gcp_spec.rb
+++ b/spec/features/projects/clusters/gcp_spec.rb
@@ -33,7 +33,8 @@ RSpec.describe 'Gcp Cluster', :js do
before do
visit project_clusters_path(project)
- click_link 'Integrate with a cluster certificate'
+ click_link 'Certificate based'
+ click_link 'Connect with a certificate'
click_link 'Create new cluster'
click_link 'Google GKE'
end
@@ -143,8 +144,9 @@ RSpec.describe 'Gcp Cluster', :js do
before do
visit project_clusters_path(project)
- click_link 'Connect cluster with certificate'
- click_link 'Connect existing cluster'
+ click_link 'Certificate based'
+ click_button(class: 'dropdown-toggle-split')
+ click_link 'Connect with certificate'
end
it 'user sees the "Environment scope" field' do
@@ -158,11 +160,12 @@ RSpec.describe 'Gcp Cluster', :js do
click_button 'Remove integration and resources'
fill_in 'confirm_cluster_name_input', with: cluster.name
click_button 'Remove integration'
+ click_link 'Certificate based'
end
it 'user sees creation form with the successful message' do
expect(page).to have_content('Kubernetes cluster integration was successfully removed.')
- expect(page).to have_link('Integrate with a cluster certificate')
+ expect(page).to have_link('Connect with a certificate')
end
end
end
@@ -171,6 +174,7 @@ RSpec.describe 'Gcp Cluster', :js do
context 'when user has not dismissed GCP signup offer' do
before do
visit project_clusters_path(project)
+ click_link 'Certificate based'
end
it 'user sees offer on cluster index page' do
@@ -178,7 +182,7 @@ RSpec.describe 'Gcp Cluster', :js do
end
it 'user sees offer on cluster create page' do
- click_link 'Integrate with a cluster certificate'
+ click_link 'Connect with a certificate'
expect(page).to have_css('.gcp-signup-offer')
end
@@ -187,6 +191,7 @@ RSpec.describe 'Gcp Cluster', :js do
context 'when user has dismissed GCP signup offer' do
before do
visit project_clusters_path(project)
+ click_link 'Certificate based'
end
it 'user does not see offer after dismissing' do
@@ -195,19 +200,18 @@ RSpec.describe 'Gcp Cluster', :js do
find('.gcp-signup-offer .js-close').click
wait_for_requests
- click_link 'Integrate with a cluster certificate'
+ click_link 'Connect with a certificate'
expect(page).not_to have_css('.gcp-signup-offer')
end
end
context 'when third party offers are disabled', :clean_gitlab_redis_shared_state do
- let(:admin) { create(:admin) }
+ let(:user) { create(:admin) }
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
- sign_in(admin)
- gitlab_enable_admin_mode_sign_in(admin)
+ gitlab_enable_admin_mode_sign_in(user)
visit general_admin_application_settings_path
end
diff --git a/spec/features/projects/clusters/user_spec.rb b/spec/features/projects/clusters/user_spec.rb
index 5b60edbcf87..d3f709bfb53 100644
--- a/spec/features/projects/clusters/user_spec.rb
+++ b/spec/features/projects/clusters/user_spec.rb
@@ -25,7 +25,8 @@ RSpec.describe 'User Cluster', :js do
before do
visit project_clusters_path(project)
- click_link 'Integrate with a cluster certificate'
+ click_link 'Certificate based'
+ click_link 'Connect with a certificate'
click_link 'Connect existing cluster'
end
@@ -112,11 +113,12 @@ RSpec.describe 'User Cluster', :js do
click_button 'Remove integration and resources'
fill_in 'confirm_cluster_name_input', with: cluster.name
click_button 'Remove integration'
+ click_link 'Certificate based'
end
it 'user sees creation form with the successful message' do
expect(page).to have_content('Kubernetes cluster integration was successfully removed.')
- expect(page).to have_link('Integrate with a cluster certificate')
+ expect(page).to have_link('Connect with a certificate')
end
end
end
diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb
index 6b03301aa74..a49fa4c9e31 100644
--- a/spec/features/projects/clusters_spec.rb
+++ b/spec/features/projects/clusters_spec.rb
@@ -16,10 +16,11 @@ RSpec.describe 'Clusters', :js do
context 'when user does not have a cluster and visits cluster index page' do
before do
visit project_clusters_path(project)
+ click_link 'Certificate based'
end
it 'sees empty state' do
- expect(page).to have_link('Integrate with a cluster certificate')
+ expect(page).to have_link('Connect with a certificate')
expect(page).to have_selector('.empty-state')
end
end
@@ -33,16 +34,17 @@ RSpec.describe 'Clusters', :js do
before do
create(:cluster, :provided_by_user, name: 'default-cluster', environment_scope: '*', projects: [project])
visit project_clusters_path(project)
+ click_link 'Certificate based'
+ click_button(class: 'dropdown-toggle-split')
end
it 'user sees an add cluster button' do
- expect(page).to have_selector('.js-add-cluster:not(.readonly)')
+ expect(page).to have_content('Connect with certificate')
end
context 'when user filled form with environment scope' do
before do
- click_link 'Connect cluster with certificate'
- click_link 'Connect existing cluster'
+ click_link 'Connect with certificate'
fill_in 'cluster_name', with: 'staging-cluster'
fill_in 'cluster_environment_scope', with: 'staging/*'
click_button 'Add Kubernetes cluster'
@@ -70,8 +72,7 @@ RSpec.describe 'Clusters', :js do
context 'when user updates duplicated environment scope' do
before do
- click_link 'Connect cluster with certificate'
- click_link 'Connect existing cluster'
+ click_link 'Connect with certificate'
fill_in 'cluster_name', with: 'staging-cluster'
fill_in 'cluster_environment_scope', with: '*'
fill_in 'cluster_platform_kubernetes_attributes_api_url', with: 'https://0.0.0.0'
@@ -108,15 +109,12 @@ RSpec.describe 'Clusters', :js do
create(:cluster, :provided_by_gcp, name: 'default-cluster', environment_scope: '*', projects: [project])
visit project_clusters_path(project)
- end
-
- it 'user sees a add cluster button' do
- expect(page).to have_selector('.js-add-cluster:not(.readonly)')
+ click_link 'Certificate based'
end
context 'when user filled form with environment scope' do
before do
- click_link 'Connect cluster with certificate'
+ click_button(class: 'dropdown-toggle-split')
click_link 'Create new cluster'
click_link 'Google GKE'
@@ -161,7 +159,7 @@ RSpec.describe 'Clusters', :js do
context 'when user updates duplicated environment scope' do
before do
- click_link 'Connect cluster with certificate'
+ click_button(class: 'dropdown-toggle-split')
click_link 'Create new cluster'
click_link 'Google GKE'
@@ -192,6 +190,7 @@ RSpec.describe 'Clusters', :js do
before do
visit project_clusters_path(project)
+ click_link 'Certificate based'
end
it 'user sees a table with one cluster' do
@@ -214,7 +213,8 @@ RSpec.describe 'Clusters', :js do
before do
visit project_clusters_path(project)
- click_link 'Integrate with a cluster certificate'
+ click_link 'Certificate based'
+ click_link 'Connect with a certificate'
click_link 'Create new cluster'
end
diff --git a/spec/features/projects/commit/comments/user_deletes_comments_spec.rb b/spec/features/projects/commit/comments/user_deletes_comments_spec.rb
index 431cbb4ffbb..67d3276fc14 100644
--- a/spec/features/projects/commit/comments/user_deletes_comments_spec.rb
+++ b/spec/features/projects/commit/comments/user_deletes_comments_spec.rb
@@ -11,6 +11,7 @@ RSpec.describe "User deletes comments on a commit", :js do
let(:user) { create(:user) }
before do
+ stub_feature_flags(bootstrap_confirmation_modals: false)
sign_in(user)
project.add_developer(user)
diff --git a/spec/features/projects/commit/user_comments_on_commit_spec.rb b/spec/features/projects/commit/user_comments_on_commit_spec.rb
index 6997c2d8338..b0be6edb245 100644
--- a/spec/features/projects/commit/user_comments_on_commit_spec.rb
+++ b/spec/features/projects/commit/user_comments_on_commit_spec.rb
@@ -93,6 +93,8 @@ RSpec.describe "User comments on commit", :js do
context "when deleting comment" do
before do
+ stub_feature_flags(bootstrap_confirmation_modals: false)
+
visit(project_commit_path(project, sample_commit.id))
add_note(comment_text)
diff --git a/spec/features/projects/confluence/user_views_confluence_page_spec.rb b/spec/features/projects/confluence/user_views_confluence_page_spec.rb
index ece2f82f5c6..49e7839f16c 100644
--- a/spec/features/projects/confluence/user_views_confluence_page_spec.rb
+++ b/spec/features/projects/confluence/user_views_confluence_page_spec.rb
@@ -16,9 +16,12 @@ RSpec.describe 'User views the Confluence page' do
visit project_wikis_confluence_path(project)
+ expect(page).to have_css('.nav-sidebar li.active', text: 'Confluence', match: :first)
+
element = page.find('.row.empty-state')
expect(element).to have_link('Go to Confluence', href: service.confluence_url)
+ expect(element).to have_link('Confluence epic', href: 'https://gitlab.com/groups/gitlab-org/-/epics/3629')
end
it 'does not show the page when the Confluence integration disabled' do
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index 5320f68b525..bcbf2f46f79 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -23,10 +23,6 @@ RSpec.describe 'Environment' do
let!(:action) { }
let!(:cluster) { }
- before do
- visit_environment(environment)
- end
-
context 'with auto-stop' do
let!(:environment) { create(:environment, :will_auto_stop, name: 'staging', project: project) }
@@ -52,12 +48,20 @@ RSpec.describe 'Environment' do
end
context 'without deployments' do
+ before do
+ visit_environment(environment)
+ end
+
it 'does not show deployments' do
expect(page).to have_content('You don\'t have any deployments right now.')
end
end
context 'with deployments' do
+ before do
+ visit_environment(environment)
+ end
+
context 'when there is no related deployable' do
let(:deployment) do
create(:deployment, :success, environment: environment, deployable: nil)
@@ -108,6 +112,26 @@ RSpec.describe 'Environment' do
end
end
+ context 'with many deployments' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
+ let!(:second) { create(:deployment, environment: environment, deployable: build, status: :success, finished_at: Time.current) }
+ let!(:first) { create(:deployment, environment: environment, deployable: build, status: :running) }
+ let!(:last) { create(:deployment, environment: environment, deployable: build, status: :success, finished_at: 2.days.ago) }
+ let!(:third) { create(:deployment, environment: environment, deployable: build, status: :canceled, finished_at: 1.day.ago) }
+
+ before do
+ visit_environment(environment)
+ end
+
+ it 'shows all of them in ordered way' do
+ ids = find_all('[data-testid="deployment-id"]').map { |e| e.text }
+ expected_ordered_ids = [first, second, third, last].map { |d| "##{d.iid}" }
+ expect(ids).to eq(expected_ordered_ids)
+ end
+ end
+
context 'with related deployable present' do
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline) }
@@ -116,6 +140,10 @@ RSpec.describe 'Environment' do
create(:deployment, :success, environment: environment, deployable: build)
end
+ before do
+ visit_environment(environment)
+ end
+
it 'does show build name' do
expect(page).to have_link("#{build.name} (##{build.id})")
end
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index 34e2ca7c8a7..3b83c25b629 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -8,6 +8,7 @@ RSpec.describe 'Environments page', :js do
let(:role) { :developer }
before do
+ stub_feature_flags(new_environments_table: false)
project.add_role(user, role)
sign_in(user)
end
@@ -142,6 +143,8 @@ RSpec.describe 'Environments page', :js do
create(:environment, project: project, state: :available)
end
+ stub_feature_flags(bootstrap_confirmation_modals: false)
+
context 'when there are no deployments' do
before do
visit_environments(project)
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index 00e85a215b8..3afd1937652 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe 'Import/Export - project import integration test', :js do
it 'user imports an exported project successfully', :sidekiq_might_not_need_inline do
visit new_project_path
- click_import_project
+ click_link 'Import project'
click_link 'GitLab export'
fill_in :name, with: 'Test Project Name', visible: true
@@ -50,7 +50,7 @@ RSpec.describe 'Import/Export - project import integration test', :js do
visit new_project_path
- click_import_project
+ click_link 'Import project'
click_link 'GitLab export'
fill_in :name, with: project.name, visible: true
attach_file('file', file)
@@ -61,8 +61,4 @@ RSpec.describe 'Import/Export - project import integration test', :js do
end
end
end
-
- def click_import_project
- find('[data-qa-panel-name="import_project"]').click # rubocop:disable QA/SelectorUsage
- end
end
diff --git a/spec/features/projects/infrastructure_registry_spec.rb b/spec/features/projects/infrastructure_registry_spec.rb
index ee35e02b5e8..27d0866bc69 100644
--- a/spec/features/projects/infrastructure_registry_spec.rb
+++ b/spec/features/projects/infrastructure_registry_spec.rb
@@ -43,7 +43,7 @@ RSpec.describe 'Infrastructure Registry' do
expect(page).to have_current_path(project_infrastructure_registry_path(terraform_module.project, terraform_module))
- expect(page).to have_css('.packages-app h1[data-testid="title"]', text: terraform_module.name)
+ expect(page).to have_css('.packages-app h2[data-testid="title"]', text: terraform_module.name)
expect(page).to have_content('Provision instructions')
expect(page).to have_content('Registry setup')
diff --git a/spec/features/projects/integrations/user_uses_inherited_settings_spec.rb b/spec/features/projects/integrations/user_uses_inherited_settings_spec.rb
index f46cade9d5f..d2c4418f0d6 100644
--- a/spec/features/projects/integrations/user_uses_inherited_settings_spec.rb
+++ b/spec/features/projects/integrations/user_uses_inherited_settings_spec.rb
@@ -84,7 +84,7 @@ RSpec.describe 'User uses inherited settings', :js do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:parent_settings) { { url: 'http://group.com', password: 'group' } }
- let_it_be(:parent_integration) { create(:jira_integration, group: group, project: nil, **parent_settings) }
+ let_it_be(:parent_integration) { create(:jira_integration, :group, group: group, **parent_settings) }
it_behaves_like 'inherited settings'
end
diff --git a/spec/features/projects/jobs/user_browses_job_spec.rb b/spec/features/projects/jobs/user_browses_job_spec.rb
index 060b7ffbfc9..12e88bbf6a5 100644
--- a/spec/features/projects/jobs/user_browses_job_spec.rb
+++ b/spec/features/projects/jobs/user_browses_job_spec.rb
@@ -12,6 +12,7 @@ RSpec.describe 'User browses a job', :js do
before do
project.add_maintainer(user)
project.enable_ci
+ stub_feature_flags(bootstrap_confirmation_modals: false)
sign_in(user)
@@ -36,8 +37,18 @@ RSpec.describe 'User browses a job', :js do
expect(page).to have_content('Job has been erased')
end
- context 'with a failed job' do
- let!(:build) { create(:ci_build, :failed, :trace_artifact, pipeline: pipeline) }
+ context 'with unarchived trace artifact' do
+ let!(:build) { create(:ci_build, :success, :unarchived_trace_artifact, :coverage, pipeline: pipeline) }
+
+ it 'shows no trace message', :js do
+ wait_for_requests
+
+ expect(page).to have_content('This job does not have a trace.')
+ end
+ end
+
+ context 'with a failed job and live trace' do
+ let!(:build) { create(:ci_build, :failed, :trace_live, pipeline: pipeline) }
it 'displays the failure reason' do
wait_for_all_requests
@@ -46,6 +57,18 @@ RSpec.describe 'User browses a job', :js do
".build-job > a[title='test - failed - (unknown failure)']")
end
end
+
+ context 'with unarchived trace artifact' do
+ let!(:artifact) { create(:ci_job_artifact, :unarchived_trace_artifact, job: build) }
+
+ it 'displays the failure reason from the live trace' do
+ wait_for_all_requests
+ within('.builds-container') do
+ expect(page).to have_selector(
+ ".build-job > a[title='test - failed - (unknown failure)']")
+ end
+ end
+ end
end
context 'when a failed job has been retried' do
diff --git a/spec/features/projects/jobs/user_triggers_manual_job_with_variables_spec.rb b/spec/features/projects/jobs/user_triggers_manual_job_with_variables_spec.rb
new file mode 100644
index 00000000000..e8a14694d88
--- /dev/null
+++ b/spec/features/projects/jobs/user_triggers_manual_job_with_variables_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'User triggers manual job with variables', :js do
+ let(:user) { create(:user) }
+ let(:user_access_level) { :developer }
+ let(:project) { create(:project, :repository, namespace: user.namespace) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.sha, ref: 'master') }
+ let!(:build) { create(:ci_build, :manual, pipeline: pipeline) }
+
+ before do
+ project.add_maintainer(user)
+ project.enable_ci
+
+ sign_in(user)
+
+ visit(project_job_path(project, build))
+ end
+
+ it 'passes values correctly' do
+ page.within(find("[data-testid='ci-variable-row']")) do
+ find("[data-testid='ci-variable-key']").set('key_name')
+ find("[data-testid='ci-variable-value']").set('key_value')
+ end
+
+ find("[data-testid='trigger-manual-job-btn']").click
+
+ wait_for_requests
+
+ expect(build.job_variables.as_json).to contain_exactly(
+ hash_including('key' => 'key_name', 'value' => 'key_value'))
+ end
+end
diff --git a/spec/features/projects/members/member_leaves_project_spec.rb b/spec/features/projects/members/member_leaves_project_spec.rb
index c4bd0b81dc0..4881a7bdf1a 100644
--- a/spec/features/projects/members/member_leaves_project_spec.rb
+++ b/spec/features/projects/members/member_leaves_project_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe 'Projects > Members > Member leaves project' do
before do
project.add_developer(user)
sign_in(user)
+ stub_feature_flags(bootstrap_confirmation_modals: false)
end
it 'user leaves project' do
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
index 113ba692497..dcaef5f4ef0 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -11,6 +11,7 @@ RSpec.describe 'Projects > Members > User requests access', :js do
before do
sign_in(user)
visit project_path(project)
+ stub_feature_flags(bootstrap_confirmation_modals: false)
end
it 'request access feature is disabled' do
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
index dacbaa826a0..4dedd5689de 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe 'New project', :js do
)
visit new_project_path
- find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
+ click_link 'Create blank project'
expect(page).to have_content 'Other visibility settings have been disabled by the administrator.'
end
@@ -34,7 +34,7 @@ RSpec.describe 'New project', :js do
)
visit new_project_path
- find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
+ click_link 'Create blank project'
expect(page).to have_content 'Visibility settings have been disabled by the administrator.'
end
@@ -49,14 +49,14 @@ RSpec.describe 'New project', :js do
it 'shows "New project" page', :js do
visit new_project_path
- find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
+ click_link 'Create blank project'
expect(page).to have_content('Project name')
expect(page).to have_content('Project URL')
expect(page).to have_content('Project slug')
click_link('New project')
- find('[data-qa-panel-name="import_project"]').click # rubocop:disable QA/SelectorUsage
+ click_link 'Import project'
expect(page).to have_link('GitHub')
expect(page).to have_link('Bitbucket')
@@ -69,7 +69,7 @@ RSpec.describe 'New project', :js do
before do
visit new_project_path
- find('[data-qa-panel-name="import_project"]').click # rubocop:disable QA/SelectorUsage
+ click_link 'Import project'
end
it 'has Manifest file' do
@@ -83,7 +83,7 @@ RSpec.describe 'New project', :js do
stub_application_setting(default_project_visibility: level)
visit new_project_path
- find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
+ click_link 'Create blank project'
page.within('#blank-project-pane') do
expect(find_field("project_visibility_level_#{level}")).to be_checked
end
@@ -91,7 +91,7 @@ RSpec.describe 'New project', :js do
it "saves visibility level #{level} on validation error" do
visit new_project_path
- find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
+ click_link 'Create blank project'
choose(key)
click_button('Create project')
@@ -111,7 +111,7 @@ RSpec.describe 'New project', :js do
context 'when admin mode is enabled', :enable_admin_mode do
it 'has private selected' do
visit new_project_path(namespace_id: group.id)
- find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
+ click_link 'Create blank project'
page.within('#blank-project-pane') do
expect(find_field("project_visibility_level_#{Gitlab::VisibilityLevel::PRIVATE}")).to be_checked
@@ -138,7 +138,7 @@ RSpec.describe 'New project', :js do
context 'when admin mode is enabled', :enable_admin_mode do
it 'has private selected' do
visit new_project_path(namespace_id: group.id, project: { visibility_level: Gitlab::VisibilityLevel::PRIVATE })
- find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
+ click_link 'Create blank project'
page.within('#blank-project-pane') do
expect(find_field("project_visibility_level_#{Gitlab::VisibilityLevel::PRIVATE}")).to be_checked
@@ -159,7 +159,7 @@ RSpec.describe 'New project', :js do
context 'Readme selector' do
it 'shows the initialize with Readme checkbox on "Blank project" tab' do
visit new_project_path
- find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
+ click_link 'Create blank project'
expect(page).to have_css('input#project_initialize_with_readme')
expect(page).to have_content('Initialize repository with a README')
@@ -167,7 +167,7 @@ RSpec.describe 'New project', :js do
it 'does not show the initialize with Readme checkbox on "Create from template" tab' do
visit new_project_path
- find('[data-qa-panel-name="create_from_template"]').click # rubocop:disable QA/SelectorUsage
+ click_link 'Create from template'
first('.choose-template').click
page.within '.project-fields-form' do
@@ -178,7 +178,7 @@ RSpec.describe 'New project', :js do
it 'does not show the initialize with Readme checkbox on "Import project" tab' do
visit new_project_path
- find('[data-qa-panel-name="import_project"]').click # rubocop:disable QA/SelectorUsage
+ click_link 'Import project'
first('.js-import-git-toggle-button').click
page.within '#import-project-pane' do
@@ -192,7 +192,7 @@ RSpec.describe 'New project', :js do
context 'with user namespace' do
before do
visit new_project_path
- find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
+ click_link 'Create blank project'
end
it 'selects the user namespace' do
@@ -208,7 +208,7 @@ RSpec.describe 'New project', :js do
before do
group.add_owner(user)
visit new_project_path(namespace_id: group.id)
- find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
+ click_link 'Create blank project'
end
it 'selects the group namespace' do
@@ -225,7 +225,7 @@ RSpec.describe 'New project', :js do
before do
group.add_maintainer(user)
visit new_project_path(namespace_id: subgroup.id)
- find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
+ click_link 'Create blank project'
end
it 'selects the group namespace' do
@@ -245,7 +245,7 @@ RSpec.describe 'New project', :js do
internal_group.add_owner(user)
private_group.add_owner(user)
visit new_project_path(namespace_id: public_group.id)
- find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
+ click_link 'Create blank project'
end
it 'enables the correct visibility options' do
@@ -275,7 +275,7 @@ RSpec.describe 'New project', :js do
context 'Import project options', :js do
before do
visit new_project_path
- find('[data-qa-panel-name="import_project"]').click # rubocop:disable QA/SelectorUsage
+ click_link 'Import project'
end
context 'from git repository url, "Repo by URL"' do
@@ -351,7 +351,7 @@ RSpec.describe 'New project', :js do
before do
group.add_developer(user)
visit new_project_path(namespace_id: group.id)
- find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
+ click_link 'Create blank project'
end
it 'selects the group namespace' do
diff --git a/spec/features/projects/packages_spec.rb b/spec/features/projects/packages_spec.rb
index 9b1e87192f5..7fcc8200b1c 100644
--- a/spec/features/projects/packages_spec.rb
+++ b/spec/features/projects/packages_spec.rb
@@ -27,10 +27,6 @@ RSpec.describe 'Packages' do
context 'when feature is available', :js do
before do
- # we are simply setting the featrure flag to false because the new UI has nothing to test yet
- # when the refactor is complete or almost complete we will turn on the feature tests
- # see https://gitlab.com/gitlab-org/gitlab/-/issues/330846 for status of this work
- stub_feature_flags(package_list_apollo: false)
visit_project_packages
end
diff --git a/spec/features/projects/pages/user_adds_domain_spec.rb b/spec/features/projects/pages/user_adds_domain_spec.rb
index de9effe3dc7..06f130ae69c 100644
--- a/spec/features/projects/pages/user_adds_domain_spec.rb
+++ b/spec/features/projects/pages/user_adds_domain_spec.rb
@@ -14,6 +14,8 @@ RSpec.describe 'User adds pages domain', :js do
project.add_maintainer(user)
sign_in(user)
+
+ stub_feature_flags(bootstrap_confirmation_modals: false)
end
context 'when pages are exposed on external HTTP address', :http_pages_enabled do
diff --git a/spec/features/projects/pages/user_edits_lets_encrypt_settings_spec.rb b/spec/features/projects/pages/user_edits_lets_encrypt_settings_spec.rb
index cf8438d5e6f..a3fc5804e13 100644
--- a/spec/features/projects/pages/user_edits_lets_encrypt_settings_spec.rb
+++ b/spec/features/projects/pages/user_edits_lets_encrypt_settings_spec.rb
@@ -14,6 +14,7 @@ RSpec.describe "Pages with Let's Encrypt", :https_pages_enabled do
before do
allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
stub_lets_encrypt_settings
+ stub_feature_flags(bootstrap_confirmation_modals: false)
project.add_role(user, role)
sign_in(user)
diff --git a/spec/features/projects/pages/user_edits_settings_spec.rb b/spec/features/projects/pages/user_edits_settings_spec.rb
index 71d4cce2784..1226e1dc2ed 100644
--- a/spec/features/projects/pages/user_edits_settings_spec.rb
+++ b/spec/features/projects/pages/user_edits_settings_spec.rb
@@ -176,6 +176,7 @@ RSpec.describe 'Pages edits pages settings', :js do
describe 'Remove page' do
context 'when pages are deployed' do
before do
+ stub_feature_flags(bootstrap_confirmation_modals: false)
project.mark_pages_as_deployed
end
diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb
index 94e3331b173..9df430c0f78 100644
--- a/spec/features/projects/pipeline_schedules_spec.rb
+++ b/spec/features/projects/pipeline_schedules_spec.rb
@@ -11,6 +11,7 @@ RSpec.describe 'Pipeline Schedules', :js do
context 'logged in as maintainer' do
before do
+ stub_feature_flags(bootstrap_confirmation_modals: false)
project.add_maintainer(user)
gitlab_sign_in(user)
end
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index bd22c8632e4..e38c4989f26 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -317,6 +317,7 @@ RSpec.describe 'Pipelines', :js do
end
before do
+ stub_feature_flags(bootstrap_confirmation_modals: false)
visit_project_pipelines
end
@@ -635,7 +636,7 @@ RSpec.describe 'Pipelines', :js do
# header
expect(page).to have_text("##{pipeline.id}")
- expect(page).to have_selector(%Q(img[alt$="#{pipeline.user.name}'s avatar"]))
+ expect(page).to have_selector(%Q(img[src="#{pipeline.user.avatar_url}"]))
expect(page).to have_link(pipeline.user.name, href: user_path(pipeline.user))
# stages
diff --git a/spec/features/projects/releases/user_views_releases_spec.rb b/spec/features/projects/releases/user_views_releases_spec.rb
index 6bc4c66b8ca..98935fdf872 100644
--- a/spec/features/projects/releases/user_views_releases_spec.rb
+++ b/spec/features/projects/releases/user_views_releases_spec.rb
@@ -123,11 +123,11 @@ RSpec.describe 'User views releases', :js do
within('.release-block', match: :first) do
expect(page).to have_content(release_v3.description)
+ expect(page).to have_content(release_v3.tag)
+ expect(page).to have_content(release_v3.name)
# The following properties (sometimes) include Git info,
# so they are not rendered for Guest users
- expect(page).not_to have_content(release_v3.name)
- expect(page).not_to have_content(release_v3.tag)
expect(page).not_to have_content(release_v3.commit.short_id)
end
end
diff --git a/spec/features/projects/settings/access_tokens_spec.rb b/spec/features/projects/settings/access_tokens_spec.rb
index 4941b936c0c..d8de9e0449e 100644
--- a/spec/features/projects/settings/access_tokens_spec.rb
+++ b/spec/features/projects/settings/access_tokens_spec.rb
@@ -13,6 +13,7 @@ RSpec.describe 'Project > Settings > Access Tokens', :js do
end
before do
+ stub_feature_flags(bootstrap_confirmation_modals: false)
sign_in(user)
end
diff --git a/spec/features/projects/settings/packages_settings_spec.rb b/spec/features/projects/settings/packages_settings_spec.rb
index 62f31fd027b..e70839e9720 100644
--- a/spec/features/projects/settings/packages_settings_spec.rb
+++ b/spec/features/projects/settings/packages_settings_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe 'Projects > Settings > Packages', :js do
let(:packages_enabled) { true }
it 'displays the packages toggle button' do
- expect(page).to have_button('Packages', class: 'gl-toggle')
+ expect(page).to have_selector('[data-testid="toggle-label"]', text: 'Packages')
expect(page).to have_selector('input[name="project[packages_enabled]"] + button', visible: true)
end
end
@@ -28,7 +28,7 @@ RSpec.describe 'Projects > Settings > Packages', :js do
let(:packages_enabled) { false }
it 'does not show up in UI' do
- expect(page).not_to have_button('Packages', class: 'gl-toggle')
+ expect(page).not_to have_selector('[data-testid="toggle-label"]', text: 'Packages')
end
end
end
diff --git a/spec/features/projects/settings/service_desk_setting_spec.rb b/spec/features/projects/settings/service_desk_setting_spec.rb
index 0924f8320e1..0df4bd3f0d9 100644
--- a/spec/features/projects/settings/service_desk_setting_spec.rb
+++ b/spec/features/projects/settings/service_desk_setting_spec.rb
@@ -54,7 +54,7 @@ RSpec.describe 'Service Desk Setting', :js, :clean_gitlab_redis_cache do
wait_for_requests
project.reload
- expect(find('[data-testid="incoming-email"]').value).to eq(project.service_desk_incoming_address)
+ expect(find('[data-testid="incoming-email"]').value).to eq(project.service_desk_custom_address)
page.within '#js-service-desk' do
fill_in('service-desk-project-suffix', with: 'foo')
diff --git a/spec/features/projects/settings/user_searches_in_settings_spec.rb b/spec/features/projects/settings/user_searches_in_settings_spec.rb
index 7ed96d01189..44b5464a1b0 100644
--- a/spec/features/projects/settings/user_searches_in_settings_spec.rb
+++ b/spec/features/projects/settings/user_searches_in_settings_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe 'User searches project settings', :js do
let_it_be(:project) { create(:project, :repository, namespace: user.namespace, pages_https_only: false) }
before do
+ stub_feature_flags(bootstrap_confirmation_modals: false)
sign_in(user)
end
diff --git a/spec/features/projects/settings/user_tags_project_spec.rb b/spec/features/projects/settings/user_tags_project_spec.rb
index ff19ed22744..e9a2aa29352 100644
--- a/spec/features/projects/settings/user_tags_project_spec.rb
+++ b/spec/features/projects/settings/user_tags_project_spec.rb
@@ -2,22 +2,40 @@
require 'spec_helper'
-RSpec.describe 'Projects > Settings > User tags a project' do
+RSpec.describe 'Projects > Settings > User tags a project', :js do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
+ let!(:topic) { create(:topic, name: 'topic1') }
before do
sign_in(user)
visit edit_project_path(project)
+ wait_for_all_requests
end
- it 'sets project topics' do
- fill_in 'Topics', with: 'topic1, topic2'
+ it 'select existing topic' do
+ fill_in class: 'gl-token-selector-input', with: 'topic1'
+ wait_for_all_requests
+
+ find('.gl-avatar-labeled[entity-name="topic1"]').click
+
+ page.within '.general-settings' do
+ click_button 'Save changes'
+ end
+
+ expect(find('#project_topic_list_field', visible: :hidden).value).to eq 'topic1'
+ end
+
+ it 'select new topic' do
+ fill_in class: 'gl-token-selector-input', with: 'topic2'
+ wait_for_all_requests
+
+ click_button 'Add "topic2"'
page.within '.general-settings' do
click_button 'Save changes'
end
- expect(find_field('Topics').value).to eq 'topic1, topic2'
+ expect(find('#project_topic_list_field', visible: :hidden).value).to eq 'topic2'
end
end
diff --git a/spec/features/projects/show/no_password_spec.rb b/spec/features/projects/show/no_password_spec.rb
index d18ff75b324..ed06f4e14d3 100644
--- a/spec/features/projects/show/no_password_spec.rb
+++ b/spec/features/projects/show/no_password_spec.rb
@@ -3,6 +3,9 @@
require 'spec_helper'
RSpec.describe 'No Password Alert' do
+ let_it_be(:message_password_auth_enabled) { 'Your account is authenticated with SSO or SAML. To push and pull over HTTP with Git using this account, you must set a password or set up a Personal Access Token to use instead of a password. For more information, see Clone with HTTPS.' }
+ let_it_be(:message_password_auth_disabled) { 'Your account is authenticated with SSO or SAML. To push and pull over HTTP with Git using this account, you must set up a Personal Access Token to use instead of a password. For more information, see Clone with HTTPS.' }
+
let(:project) { create(:project, :repository, namespace: user.namespace) }
context 'with internal auth enabled' do
@@ -15,7 +18,7 @@ RSpec.describe 'No Password Alert' do
let(:user) { create(:user) }
it 'shows no alert' do
- expect(page).not_to have_content "You won't be able to pull or push repositories via HTTP until you set a password on your account"
+ expect(page).not_to have_content message_password_auth_enabled
end
end
@@ -23,7 +26,7 @@ RSpec.describe 'No Password Alert' do
let(:user) { create(:user, password_automatically_set: true) }
it 'shows a password alert' do
- expect(page).to have_content "You won't be able to pull or push repositories via HTTP until you set a password on your account"
+ expect(page).to have_content message_password_auth_enabled
end
end
end
@@ -41,7 +44,7 @@ RSpec.describe 'No Password Alert' do
gitlab_sign_in_via('saml', user, 'my-uid')
visit project_path(project)
- expect(page).to have_content "You won't be able to pull or push repositories via HTTP until you create a personal access token on your account"
+ expect(page).to have_content message_password_auth_disabled
end
end
@@ -51,7 +54,7 @@ RSpec.describe 'No Password Alert' do
gitlab_sign_in_via('saml', user, 'my-uid')
visit project_path(project)
- expect(page).not_to have_content "You won't be able to pull or push repositories via HTTP until you create a personal access token on your account"
+ expect(page).not_to have_content message_password_auth_disabled
end
end
end
diff --git a/spec/features/projects/show/user_uploads_files_spec.rb b/spec/features/projects/show/user_uploads_files_spec.rb
index 51e41397439..92b54d83ef3 100644
--- a/spec/features/projects/show/user_uploads_files_spec.rb
+++ b/spec/features/projects/show/user_uploads_files_spec.rb
@@ -44,27 +44,27 @@ RSpec.describe 'Projects > Show > User uploads files' do
end
end
- context 'when in the empty_repo_upload experiment' do
- before do
- stub_experiments(empty_repo_upload: :candidate)
+ context 'with an empty repo' do
+ let(:project) { create(:project, :empty_repo, creator: user) }
+ before do
visit(project_path(project))
end
- context 'with an empty repo' do
- let(:project) { create(:project, :empty_repo, creator: user) }
-
- [true, false].each do |value|
- include_examples 'uploads and commits a new text file via "upload file" button', drop: value
- end
+ [true, false].each do |value|
+ include_examples 'uploads and commits a new text file via "upload file" button', drop: value
end
+ end
- context 'with a nonempty repo' do
- let(:project) { create(:project, :repository, creator: user) }
+ context 'with a nonempty repo' do
+ let(:project) { create(:project, :repository, creator: user) }
- [true, false].each do |value|
- include_examples 'uploads and commits a new text file via "upload file" button', drop: value
- end
+ before do
+ visit(project_path(project))
+ end
+
+ [true, false].each do |value|
+ include_examples 'uploads and commits a new text file via "upload file" button', drop: value
end
end
end
diff --git a/spec/features/projects/user_changes_project_visibility_spec.rb b/spec/features/projects/user_changes_project_visibility_spec.rb
index 39b8cddd005..345d16982fd 100644
--- a/spec/features/projects/user_changes_project_visibility_spec.rb
+++ b/spec/features/projects/user_changes_project_visibility_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe 'User changes public project visibility', :js do
click_button 'Save changes'
end
- find('.js-confirm-danger-input').send_keys(project.path_with_namespace)
+ find('.js-legacy-confirm-danger-input').send_keys(project.path_with_namespace)
page.within '.modal' do
click_button 'Reduce project visibility'
diff --git a/spec/features/projects/user_creates_project_spec.rb b/spec/features/projects/user_creates_project_spec.rb
index 5d482f9fbd0..f5e8a5e8fc1 100644
--- a/spec/features/projects/user_creates_project_spec.rb
+++ b/spec/features/projects/user_creates_project_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe 'User creates a project', :js do
it 'creates a new project' do
visit(new_project_path)
- find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
+ click_link 'Create blank project'
fill_in(:project_name, with: 'Empty')
expect(page).to have_checked_field 'Initialize repository with a README'
@@ -38,7 +38,7 @@ RSpec.describe 'User creates a project', :js do
visit(new_project_path)
- find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
+ click_link 'Create blank project'
fill_in(:project_name, with: 'With initial commits')
expect(page).to have_checked_field 'Initialize repository with a README'
@@ -67,7 +67,7 @@ RSpec.describe 'User creates a project', :js do
it 'creates a new project' do
visit(new_project_path)
- find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
+ click_link 'Create blank project'
fill_in :project_name, with: 'A Subgroup Project'
fill_in :project_path, with: 'a-subgroup-project'
@@ -96,7 +96,7 @@ RSpec.describe 'User creates a project', :js do
it 'creates a new project' do
visit(new_project_path)
- find('[data-qa-panel-name="blank_project"]').click # rubocop:disable QA/SelectorUsage
+ click_link 'Create blank project'
fill_in :project_name, with: 'a-new-project'
fill_in :project_path, with: 'a-new-project'
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 59ad7d31ea7..c4619b5498e 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe 'Project' do
shared_examples 'creates from template' do |template, sub_template_tab = nil|
it "is created from template", :js do
- find('[data-qa-panel-name="create_from_template"]').click # rubocop:disable QA/SelectorUsage
+ click_link 'Create from template'
find(".project-template #{sub_template_tab}").click if sub_template_tab
find("label[for=#{template.name}]").click
fill_in("project_name", with: template.name)
@@ -133,7 +133,7 @@ RSpec.describe 'Project' do
visit path
expect(page).to have_selector('[data-testid="project_topic_list"]')
- expect(page).to have_link('topic1', href: explore_projects_path(topic: 'topic1'))
+ expect(page).to have_link('topic1', href: topic_explore_projects_path(topic_name: 'topic1'))
end
it 'shows up to 3 project topics' do
@@ -142,9 +142,9 @@ RSpec.describe 'Project' do
visit path
expect(page).to have_selector('[data-testid="project_topic_list"]')
- expect(page).to have_link('topic1', href: explore_projects_path(topic: 'topic1'))
- expect(page).to have_link('topic2', href: explore_projects_path(topic: 'topic2'))
- expect(page).to have_link('topic3', href: explore_projects_path(topic: 'topic3'))
+ expect(page).to have_link('topic1', href: topic_explore_projects_path(topic_name: 'topic1'))
+ expect(page).to have_link('topic2', href: topic_explore_projects_path(topic_name: 'topic2'))
+ expect(page).to have_link('topic3', href: topic_explore_projects_path(topic_name: 'topic3'))
expect(page).to have_content('+ 1 more')
end
end
@@ -257,7 +257,7 @@ RSpec.describe 'Project' do
end
it 'deletes a project', :sidekiq_inline do
- expect { remove_with_confirm('Delete project', project.path, 'Yes, delete project') }.to change { Project.count }.by(-1)
+ expect { remove_with_confirm('Delete project', project.path_with_namespace, 'Yes, delete project') }.to change { Project.count }.by(-1)
expect(page).to have_content "Project '#{project.full_name}' is in the process of being deleted."
expect(Project.all.count).to be_zero
expect(project.issues).to be_empty
diff --git a/spec/features/signed_commits_spec.rb b/spec/features/signed_commits_spec.rb
index d679e4dbb99..610a80eb12c 100644
--- a/spec/features/signed_commits_spec.rb
+++ b/spec/features/signed_commits_spec.rb
@@ -11,6 +11,7 @@ RSpec.describe 'GPG signed commits' do
perform_enqueued_jobs do
create :gpg_key, key: GpgHelpers::User1.public_key, user: user
+ user.reload # necessary to reload the association with gpg_keys
end
visit project_commit_path(project, ref)
@@ -114,6 +115,19 @@ RSpec.describe 'GPG signed commits' do
end
end
+ it 'unverified signature: commit contains multiple GPG signatures' do
+ user_1_key
+
+ visit project_commit_path(project, GpgHelpers::MULTIPLE_SIGNATURES_SHA)
+ wait_for_all_requests
+
+ page.find('.gpg-status-box', text: 'Unverified').click
+
+ within '.popover' do
+ expect(page).to have_content "This commit was signed with multiple signatures."
+ end
+ end
+
it 'verified and the gpg user has a gitlab profile' do
user_1_key
@@ -168,7 +182,7 @@ RSpec.describe 'GPG signed commits' do
page.find('.gpg-status-box', text: 'Unverified').click
within '.popover' do
- expect(page).to have_content 'This commit was signed with an unverified signature'
+ expect(page).to have_content 'This commit was signed with multiple signatures.'
end
end
end
diff --git a/spec/features/snippets/notes_on_personal_snippets_spec.rb b/spec/features/snippets/notes_on_personal_snippets_spec.rb
index fc88cd9205c..6bd31d7314c 100644
--- a/spec/features/snippets/notes_on_personal_snippets_spec.rb
+++ b/spec/features/snippets/notes_on_personal_snippets_spec.rb
@@ -18,6 +18,7 @@ RSpec.describe 'Comments on personal snippets', :js do
end
before do
+ stub_feature_flags(bootstrap_confirmation_modals: false)
sign_in user
visit snippet_path(snippet)
diff --git a/spec/features/snippets/user_creates_snippet_spec.rb b/spec/features/snippets/user_creates_snippet_spec.rb
index ca050daa62a..82fe895d397 100644
--- a/spec/features/snippets/user_creates_snippet_spec.rb
+++ b/spec/features/snippets/user_creates_snippet_spec.rb
@@ -16,6 +16,7 @@ RSpec.describe 'User creates snippet', :js do
let(:snippet_title_field) { 'snippet-title' }
before do
+ stub_feature_flags(bootstrap_confirmation_modals: false)
sign_in(user)
visit new_snippet_path
diff --git a/spec/features/topic_show_spec.rb b/spec/features/topic_show_spec.rb
new file mode 100644
index 00000000000..3a9865a6503
--- /dev/null
+++ b/spec/features/topic_show_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Topic show page' do
+ let_it_be(:topic) { create(:topic, name: 'my-topic', description: 'This is **my** topic https://google.com/ :poop: ```\ncode\n```', avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) }
+
+ context 'when topic does not exist' do
+ let(:path) { topic_explore_projects_path(topic_name: 'non-existing') }
+
+ it 'renders 404' do
+ visit path
+
+ expect(status_code).to eq(404)
+ end
+ end
+
+ context 'when topic exists' do
+ before do
+ visit topic_explore_projects_path(topic_name: topic.name)
+ end
+
+ it 'shows name, avatar and description as markdown' do
+ expect(page).to have_content(topic.name)
+ expect(page).to have_selector('.avatar-container > img.topic-avatar')
+ expect(find('.topic-description')).to have_selector('p > strong')
+ expect(find('.topic-description')).to have_selector('p > a[rel]')
+ expect(find('.topic-description')).to have_selector('p > gl-emoji')
+ expect(find('.topic-description')).to have_selector('p > code')
+ end
+
+ context 'with associated projects' do
+ let!(:project) { create(:project, :public, topic_list: topic.name) }
+
+ it 'shows project list' do
+ visit topic_explore_projects_path(topic_name: topic.name)
+
+ expect(find('.projects-list .project-name')).to have_content(project.name)
+ end
+ end
+
+ context 'without associated projects' do
+ it 'shows correct empty state message' do
+ expect(page).to have_content('Explore public groups to find projects to contribute to.')
+ end
+ end
+ end
+end
diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb
index 6fa805d8c74..2ddd86dd807 100644
--- a/spec/features/triggers_spec.rb
+++ b/spec/features/triggers_spec.rb
@@ -72,6 +72,7 @@ RSpec.describe 'Triggers', :js do
describe 'trigger "Revoke" workflow' do
before do
+ stub_feature_flags(bootstrap_confirmation_modals: false)
create(:ci_trigger, owner: user2, project: @project, description: trigger_title)
visit project_settings_ci_cd_path(@project)
end
diff --git a/spec/features/users/confirmation_spec.rb b/spec/features/users/confirmation_spec.rb
new file mode 100644
index 00000000000..aaa49c75223
--- /dev/null
+++ b/spec/features/users/confirmation_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'User confirmation' do
+ describe 'resend confirmation instructions' do
+ context 'when recaptcha is enabled' do
+ before do
+ stub_application_setting(recaptcha_enabled: true)
+ allow(Gitlab::Recaptcha).to receive(:load_configurations!)
+ visit new_user_confirmation_path
+ end
+
+ it 'renders recaptcha' do
+ expect(page).to have_css('.g-recaptcha')
+ end
+ end
+
+ context 'when recaptcha is not enabled' do
+ before do
+ stub_application_setting(recaptcha_enabled: false)
+ visit new_user_confirmation_path
+ end
+
+ it 'does not render recaptcha' do
+ expect(page).not_to have_css('.g-recaptcha')
+ end
+ end
+ end
+end
diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb
index 10c1c2cb26e..66ebd00d368 100644
--- a/spec/features/users/login_spec.rb
+++ b/spec/features/users/login_spec.rb
@@ -753,7 +753,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_shared_state do
end
end
- context 'when terms are enforced' do
+ context 'when terms are enforced', :js do
let(:user) { create(:user) }
before do
@@ -802,7 +802,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_shared_state do
end
context 'when the user did not enable 2FA' do
- it 'asks to set 2FA before asking to accept the terms', :js do
+ it 'asks to set 2FA before asking to accept the terms' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
@@ -887,7 +887,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_shared_state do
end
end
- context 'when the user does not have an email configured', :js do
+ context 'when the user does not have an email configured' do
let(:user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'saml', email: 'temp-email-for-oauth-user@gitlab.localhost') }
before do
diff --git a/spec/features/users/password_spec.rb b/spec/features/users/password_spec.rb
new file mode 100644
index 00000000000..793a11c616e
--- /dev/null
+++ b/spec/features/users/password_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'User password' do
+ describe 'send password reset' do
+ context 'when recaptcha is enabled' do
+ before do
+ stub_application_setting(recaptcha_enabled: true)
+ allow(Gitlab::Recaptcha).to receive(:load_configurations!)
+ visit new_user_password_path
+ end
+
+ it 'renders recaptcha' do
+ expect(page).to have_css('.g-recaptcha')
+ end
+ end
+
+ context 'when recaptcha is not enabled' do
+ before do
+ stub_application_setting(recaptcha_enabled: false)
+ visit new_user_password_path
+ end
+
+ it 'does not render recaptcha' do
+ expect(page).not_to have_css('.g-recaptcha')
+ end
+ end
+ end
+end
diff --git a/spec/features/users/terms_spec.rb b/spec/features/users/terms_spec.rb
index 8ba79d77c22..7cfe74f8aa9 100644
--- a/spec/features/users/terms_spec.rb
+++ b/spec/features/users/terms_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Users > Terms' do
+RSpec.describe 'Users > Terms', :js do
include TermsHelper
let!(:term) { create(:term, terms: 'By accepting, you promise to be nice!') }
diff --git a/spec/finders/autocomplete/routes_finder_spec.rb b/spec/finders/autocomplete/routes_finder_spec.rb
new file mode 100644
index 00000000000..c5b040a5640
--- /dev/null
+++ b/spec/finders/autocomplete/routes_finder_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Autocomplete::RoutesFinder do
+ describe '#execute' do
+ let_it_be(:user) { create(:user, username: 'user_path') }
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:group) { create(:group, path: 'path1') }
+ let_it_be(:group2) { create(:group, path: 'path2') }
+ let_it_be(:group3) { create(:group, path: 'not-matching') }
+ let_it_be(:project) { create(:project, path: 'path3', namespace: user.namespace) }
+ let_it_be(:project2) { create(:project, path: 'path4') }
+ let_it_be(:project_namespace) { create(:project_namespace, parent: group, path: 'path5') }
+
+ let(:current_user) { user }
+ let(:search) { 'path' }
+
+ before do
+ group.add_owner(user)
+ end
+
+ context 'for NamespacesOnly' do
+ subject { Autocomplete::RoutesFinder::NamespacesOnly.new(current_user, search: search).execute }
+
+ let(:user_route) { Route.find_by_path(user.username) }
+
+ it 'finds only user namespace and groups matching the search excluding project namespaces' do
+ is_expected.to match_array([group.route, user_route])
+ end
+
+ context 'when user is admin' do
+ let(:current_user) { admin }
+
+ it 'finds all namespaces matching the search excluding project namespaces' do
+ is_expected.to match_array([group.route, group2.route, user_route])
+ end
+ end
+ end
+
+ context 'for ProjectsOnly' do
+ subject { Autocomplete::RoutesFinder::ProjectsOnly.new(current_user, search: 'path').execute }
+
+ it 'finds only matching projects the user has access to' do
+ is_expected.to match_array([project.route])
+ end
+
+ context 'when user is admin' do
+ let(:current_user) { admin }
+
+ it 'finds all projects matching the search' do
+ is_expected.to match_array([project.route, project2.route])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/finders/branches_finder_spec.rb b/spec/finders/branches_finder_spec.rb
index f9d525c33a4..11b7ab08fb2 100644
--- a/spec/finders/branches_finder_spec.rb
+++ b/spec/finders/branches_finder_spec.rb
@@ -208,10 +208,10 @@ RSpec.describe BranchesFinder do
context 'by page_token only' do
let(:params) { { page_token: 'feature' } }
- it 'returns nothing' do
- result = subject
-
- expect(result.count).to eq(0)
+ it 'raises an error' do
+ expect do
+ subject
+ end.to raise_error(Gitlab::Git::CommandError, '13:could not find page token.')
end
end
diff --git a/spec/finders/ci/pipelines_for_merge_request_finder_spec.rb b/spec/finders/ci/pipelines_for_merge_request_finder_spec.rb
index 8a802e9660b..a7cf041f553 100644
--- a/spec/finders/ci/pipelines_for_merge_request_finder_spec.rb
+++ b/spec/finders/ci/pipelines_for_merge_request_finder_spec.rb
@@ -135,86 +135,6 @@ RSpec.describe Ci::PipelinesForMergeRequestFinder do
end
context 'when pipelines exist for the branch and merge request' do
- shared_examples 'returns all pipelines for merge request' do
- it 'returns merge request pipeline first' do
- expect(subject.all).to eq([detached_merge_request_pipeline, branch_pipeline])
- end
-
- context 'when there are a branch pipeline and a merge request pipeline' do
- let!(:branch_pipeline_2) do
- create(:ci_pipeline, source: :push, project: project,
- ref: source_ref, sha: shas.first)
- end
-
- let!(:detached_merge_request_pipeline_2) do
- create(:ci_pipeline, source: :merge_request_event, project: project,
- ref: source_ref, sha: shas.first, merge_request: merge_request)
- end
-
- it 'returns merge request pipelines first' do
- expect(subject.all)
- .to eq([detached_merge_request_pipeline_2,
- detached_merge_request_pipeline,
- branch_pipeline_2,
- branch_pipeline])
- end
- end
-
- context 'when there are multiple merge request pipelines from the same branch' do
- let!(:branch_pipeline_2) do
- create(:ci_pipeline, source: :push, project: project,
- ref: source_ref, sha: shas.first)
- end
-
- let!(:branch_pipeline_with_sha_not_belonging_to_merge_request) do
- create(:ci_pipeline, source: :push, project: project, ref: source_ref)
- end
-
- let!(:detached_merge_request_pipeline_2) do
- create(:ci_pipeline, source: :merge_request_event, project: project,
- ref: source_ref, sha: shas.first, merge_request: merge_request_2)
- end
-
- let(:merge_request_2) do
- create(:merge_request, source_project: project, source_branch: source_ref,
- target_project: project, target_branch: 'stable')
- end
-
- before do
- shas.each.with_index do |sha, index|
- create(:merge_request_diff_commit,
- merge_request_diff: merge_request_2.merge_request_diff,
- sha: sha, relative_order: index)
- end
- end
-
- it 'returns only related merge request pipelines' do
- expect(subject.all)
- .to eq([detached_merge_request_pipeline,
- branch_pipeline_2,
- branch_pipeline])
-
- expect(described_class.new(merge_request_2, nil).all)
- .to match_array([detached_merge_request_pipeline_2, branch_pipeline_2, branch_pipeline])
- end
- end
-
- context 'when detached merge request pipeline is run on head ref of the merge request' do
- let!(:detached_merge_request_pipeline) do
- create(:ci_pipeline, source: :merge_request_event, project: project,
- ref: merge_request.ref_path, sha: shas.second, merge_request: merge_request)
- end
-
- it 'sets the head ref of the merge request to the pipeline ref' do
- expect(detached_merge_request_pipeline.ref).to match(%r{refs/merge-requests/\d+/head})
- end
-
- it 'includes the detached merge request pipeline even though the ref is custom path' do
- expect(merge_request.all_pipelines).to include(detached_merge_request_pipeline)
- end
- end
- end
-
let(:source_ref) { 'feature' }
let(:target_ref) { 'master' }
@@ -240,20 +160,76 @@ RSpec.describe Ci::PipelinesForMergeRequestFinder do
let(:project) { create(:project, :repository) }
let(:shas) { project.repository.commits(source_ref, limit: 2).map(&:id) }
- context 'when `decomposed_ci_query_in_pipelines_for_merge_request_finder` feature flag enabled' do
- before do
- stub_feature_flags(decomposed_ci_query_in_pipelines_for_merge_request_finder: merge_request.target_project)
+ it 'returns merge request pipeline first' do
+ expect(subject.all).to match_array([detached_merge_request_pipeline, branch_pipeline])
+ end
+
+ context 'when there are a branch pipeline and a merge request pipeline' do
+ let!(:branch_pipeline_2) do
+ create(:ci_pipeline, source: :push, project: project,
+ ref: source_ref, sha: shas.first)
+ end
+
+ let!(:detached_merge_request_pipeline_2) do
+ create(:ci_pipeline, source: :merge_request_event, project: project,
+ ref: source_ref, sha: shas.first, merge_request: merge_request)
end
- it_behaves_like 'returns all pipelines for merge request'
+ it 'returns merge request pipelines first' do
+ expect(subject.all)
+ .to match_array([detached_merge_request_pipeline_2, detached_merge_request_pipeline, branch_pipeline_2, branch_pipeline])
+ end
end
- context 'when `decomposed_ci_query_in_pipelines_for_merge_request_finder` feature flag disabled' do
+ context 'when there are multiple merge request pipelines from the same branch' do
+ let!(:branch_pipeline_2) do
+ create(:ci_pipeline, source: :push, project: project,
+ ref: source_ref, sha: shas.first)
+ end
+
+ let!(:branch_pipeline_with_sha_not_belonging_to_merge_request) do
+ create(:ci_pipeline, source: :push, project: project, ref: source_ref)
+ end
+
+ let!(:detached_merge_request_pipeline_2) do
+ create(:ci_pipeline, source: :merge_request_event, project: project,
+ ref: source_ref, sha: shas.first, merge_request: merge_request_2)
+ end
+
+ let(:merge_request_2) do
+ create(:merge_request, source_project: project, source_branch: source_ref,
+ target_project: project, target_branch: 'stable')
+ end
+
before do
- stub_feature_flags(decomposed_ci_query_in_pipelines_for_merge_request_finder: false)
+ shas.each.with_index do |sha, index|
+ create(:merge_request_diff_commit,
+ merge_request_diff: merge_request_2.merge_request_diff,
+ sha: sha, relative_order: index)
+ end
end
- it_behaves_like 'returns all pipelines for merge request'
+ it 'returns only related merge request pipelines' do
+ expect(subject.all).to match_array([detached_merge_request_pipeline, branch_pipeline_2, branch_pipeline])
+
+ expect(described_class.new(merge_request_2, nil).all)
+ .to match_array([detached_merge_request_pipeline_2, branch_pipeline_2, branch_pipeline])
+ end
+ end
+
+ context 'when detached merge request pipeline is run on head ref of the merge request' do
+ let!(:detached_merge_request_pipeline) do
+ create(:ci_pipeline, source: :merge_request_event, project: project,
+ ref: merge_request.ref_path, sha: shas.second, merge_request: merge_request)
+ end
+
+ it 'sets the head ref of the merge request to the pipeline ref' do
+ expect(detached_merge_request_pipeline.ref).to match(%r{refs/merge-requests/\d+/head})
+ end
+
+ it 'includes the detached merge request pipeline even though the ref is custom path' do
+ expect(merge_request.all_pipelines).to include(detached_merge_request_pipeline)
+ end
end
end
end
diff --git a/spec/finders/clusters/agent_authorizations_finder_spec.rb b/spec/finders/clusters/agent_authorizations_finder_spec.rb
new file mode 100644
index 00000000000..687906db0d7
--- /dev/null
+++ b/spec/finders/clusters/agent_authorizations_finder_spec.rb
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Clusters::AgentAuthorizationsFinder do
+ describe '#execute' do
+ let_it_be(:top_level_group) { create(:group) }
+ let_it_be(:subgroup1) { create(:group, parent: top_level_group) }
+ let_it_be(:subgroup2) { create(:group, parent: subgroup1) }
+ let_it_be(:bottom_level_group) { create(:group, parent: subgroup2) }
+
+ let_it_be(:agent_configuration_project) { create(:project, namespace: subgroup1) }
+ let_it_be(:requesting_project, reload: true) { create(:project, namespace: bottom_level_group) }
+
+ let_it_be(:staging_agent) { create(:cluster_agent, project: agent_configuration_project) }
+ let_it_be(:production_agent) { create(:cluster_agent, project: agent_configuration_project) }
+
+ subject { described_class.new(requesting_project).execute }
+
+ shared_examples_for 'access_as' do
+ let(:config) { { access_as: { access_as => {} } } }
+
+ context 'agent' do
+ let(:access_as) { :agent }
+
+ it { is_expected.to match_array [authorization] }
+ end
+
+ context 'impersonate' do
+ let(:access_as) { :impersonate }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'ci_user' do
+ let(:access_as) { :ci_user }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'ci_job' do
+ let(:access_as) { :ci_job }
+
+ it { is_expected.to be_empty }
+ end
+ end
+
+ describe 'project authorizations' do
+ context 'agent configuration project does not share a root namespace with the given project' do
+ let(:unrelated_agent) { create(:cluster_agent) }
+
+ before do
+ create(:agent_project_authorization, agent: unrelated_agent, project: requesting_project)
+ end
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'with project authorizations present' do
+ let!(:authorization) { create(:agent_project_authorization, agent: production_agent, project: requesting_project) }
+
+ it { is_expected.to match_array [authorization] }
+ end
+
+ context 'with overlapping authorizations' do
+ let!(:agent) { create(:cluster_agent, project: requesting_project) }
+ let!(:project_authorization) { create(:agent_project_authorization, agent: agent, project: requesting_project) }
+ let!(:group_authorization) { create(:agent_group_authorization, agent: agent, group: bottom_level_group) }
+
+ it { is_expected.to match_array [project_authorization] }
+ end
+
+ it_behaves_like 'access_as' do
+ let!(:authorization) { create(:agent_project_authorization, agent: production_agent, project: requesting_project, config: config) }
+ end
+ end
+
+ describe 'implicit authorizations' do
+ let!(:associated_agent) { create(:cluster_agent, project: requesting_project) }
+
+ it 'returns authorizations for agents directly associated with the project' do
+ expect(subject.count).to eq(1)
+
+ authorization = subject.first
+ expect(authorization).to be_a(Clusters::Agents::ImplicitAuthorization)
+ expect(authorization.agent).to eq(associated_agent)
+ end
+ end
+
+ describe 'authorized groups' do
+ context 'agent configuration project is outside the requesting project hierarchy' do
+ let(:unrelated_agent) { create(:cluster_agent) }
+
+ before do
+ create(:agent_group_authorization, agent: unrelated_agent, group: top_level_group)
+ end
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'multiple agents are authorized for the same group' do
+ let!(:staging_auth) { create(:agent_group_authorization, agent: staging_agent, group: bottom_level_group) }
+ let!(:production_auth) { create(:agent_group_authorization, agent: production_agent, group: bottom_level_group) }
+
+ it 'returns authorizations for all agents' do
+ expect(subject).to contain_exactly(staging_auth, production_auth)
+ end
+ end
+
+ context 'a single agent is authorized to more than one matching group' do
+ let!(:bottom_level_auth) { create(:agent_group_authorization, agent: production_agent, group: bottom_level_group) }
+ let!(:top_level_auth) { create(:agent_group_authorization, agent: production_agent, group: top_level_group) }
+
+ it 'picks the authorization for the closest group to the requesting project' do
+ expect(subject).to contain_exactly(bottom_level_auth)
+ end
+ end
+
+ it_behaves_like 'access_as' do
+ let!(:authorization) { create(:agent_group_authorization, agent: production_agent, group: top_level_group, config: config) }
+ end
+ end
+ end
+end
diff --git a/spec/finders/environments/environments_by_deployments_finder_spec.rb b/spec/finders/environments/environments_by_deployments_finder_spec.rb
index 1b86aced67d..7804ffa4ef1 100644
--- a/spec/finders/environments/environments_by_deployments_finder_spec.rb
+++ b/spec/finders/environments/environments_by_deployments_finder_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Environments::EnvironmentsByDeploymentsFinder do
project.add_maintainer(user)
end
- describe '#execute' do
+ shared_examples 'execute' do
context 'tagged deployment' do
let(:environment_two) { create(:environment, project: project) }
# Environments need to include commits, so rewind two commits to fit
@@ -124,4 +124,16 @@ RSpec.describe Environments::EnvironmentsByDeploymentsFinder do
end
end
end
+
+ describe "#execute" do
+ include_examples 'execute'
+
+ context 'when environments_by_deployments_finder_exists_optimization is disabled' do
+ before do
+ stub_feature_flags(environments_by_deployments_finder_exists_optimization: false)
+ end
+
+ include_examples 'execute'
+ end
+ end
end
diff --git a/spec/finders/members_finder_spec.rb b/spec/finders/members_finder_spec.rb
index 749e319f9c7..aa7d32e51ac 100644
--- a/spec/finders/members_finder_spec.rb
+++ b/spec/finders/members_finder_spec.rb
@@ -202,13 +202,5 @@ RSpec.describe MembersFinder, '#execute' do
end
it_behaves_like 'with invited_groups param'
-
- context 'when feature flag :linear_members_finder_ancestor_scopes is disabled' do
- before do
- stub_feature_flags(linear_members_finder_ancestor_scopes: false)
- end
-
- it_behaves_like 'with invited_groups param'
- end
end
end
diff --git a/spec/finders/tags_finder_spec.rb b/spec/finders/tags_finder_spec.rb
index fe015d53ac9..acc86547271 100644
--- a/spec/finders/tags_finder_spec.rb
+++ b/spec/finders/tags_finder_spec.rb
@@ -7,13 +7,8 @@ RSpec.describe TagsFinder do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:repository) { project.repository }
- def load_tags(params)
- tags_finder = described_class.new(repository, params)
- tags, error = tags_finder.execute
-
- expect(error).to eq(nil)
-
- tags
+ def load_tags(params, gitaly_pagination: false)
+ described_class.new(repository, params).execute(gitaly_pagination: gitaly_pagination)
end
describe '#execute' do
@@ -101,15 +96,79 @@ RSpec.describe TagsFinder do
end
end
+ context 'with Gitaly pagination' do
+ subject { load_tags(params, gitaly_pagination: true) }
+
+ context 'by page_token and per_page' do
+ let(:params) { { page_token: 'v1.0.0', per_page: 1 } }
+
+ it 'filters tags' do
+ result = subject
+
+ expect(result.map(&:name)).to eq(%w(v1.1.0))
+ end
+ end
+
+ context 'by next page_token and per_page' do
+ let(:params) { { page_token: 'v1.1.0', per_page: 2 } }
+
+ it 'filters branches' do
+ result = subject
+
+ expect(result.map(&:name)).to eq(%w(v1.1.1))
+ end
+ end
+
+ context 'by per_page only' do
+ let(:params) { { per_page: 2 } }
+
+ it 'filters branches' do
+ result = subject
+
+ expect(result.map(&:name)).to eq(%w(v1.0.0 v1.1.0))
+ end
+ end
+
+ context 'by page_token only' do
+ let(:params) { { page_token: 'feature' } }
+
+ it 'raises an error' do
+ expect do
+ subject
+ end.to raise_error(Gitlab::Git::InvalidPageToken, 'Invalid page token: refs/tags/feature')
+ end
+ end
+
+ context 'pagination and sort' do
+ context 'by per_page' do
+ let(:params) { { sort: 'updated_desc', per_page: 5 } }
+
+ it 'filters branches' do
+ result = subject
+
+ expect(result.map(&:name)).to eq(%w(v1.1.1 v1.1.0 v1.0.0))
+ end
+ end
+
+ context 'by page_token and per_page' do
+ let(:params) { { sort: 'updated_desc', page_token: 'v1.1.1', per_page: 2 } }
+
+ it 'filters branches' do
+ result = subject
+
+ expect(result.map(&:name)).to eq(%w(v1.1.0 v1.0.0))
+ end
+ end
+ end
+ end
+
context 'when Gitaly is unavailable' do
- it 'returns empty list of tags' do
+ it 'raises an exception' do
expect(Gitlab::GitalyClient).to receive(:call).and_raise(GRPC::Unavailable)
tags_finder = described_class.new(repository, {})
- tags, error = tags_finder.execute
- expect(error).to be_a(Gitlab::Git::CommandError)
- expect(tags).to eq([])
+ expect { tags_finder.execute }.to raise_error(Gitlab::Git::CommandError)
end
end
end
diff --git a/spec/fixtures/api/schemas/analytics/cycle_analytics/summary.json b/spec/fixtures/api/schemas/analytics/cycle_analytics/summary.json
index 73904438ede..296e18fca47 100644
--- a/spec/fixtures/api/schemas/analytics/cycle_analytics/summary.json
+++ b/spec/fixtures/api/schemas/analytics/cycle_analytics/summary.json
@@ -14,6 +14,9 @@
},
"unit": {
"type": "string"
+ },
+ "links": {
+ "type": "array"
}
},
"additionalProperties": false
diff --git a/spec/fixtures/api/schemas/graphql/packages/package_details.json b/spec/fixtures/api/schemas/graphql/packages/package_details.json
index 2824ca64325..9ef7f6c9271 100644
--- a/spec/fixtures/api/schemas/graphql/packages/package_details.json
+++ b/spec/fixtures/api/schemas/graphql/packages/package_details.json
@@ -12,7 +12,6 @@
"tags",
"pipelines",
"versions",
- "metadata",
"status",
"canDestroy"
],
@@ -47,7 +46,8 @@
"GENERIC",
"GOLANG",
"RUBYGEMS",
- "DEBIAN"
+ "DEBIAN",
+ "HELM"
]
},
"tags": {
diff --git a/spec/fixtures/api/schemas/public_api/v4/deploy_key.json b/spec/fixtures/api/schemas/public_api/v4/deploy_key.json
new file mode 100644
index 00000000000..3dbdfcc95a1
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/deploy_key.json
@@ -0,0 +1,25 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "title",
+ "created_at",
+ "expires_at",
+ "key",
+ "fingerprint",
+ "projects_with_write_access"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "title": { "type": "string" },
+ "created_at": { "type": "string", "format": "date-time" },
+ "expires_at": { "type": ["string", "null"], "format": "date-time" },
+ "key": { "type": "string" },
+ "fingerprint": { "type": "string" },
+ "projects_with_write_access": {
+ "type": "array",
+ "items": { "$ref": "project/identity.json" }
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/deploy_keys.json b/spec/fixtures/api/schemas/public_api/v4/deploy_keys.json
new file mode 100644
index 00000000000..82ddbdddbee
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/deploy_keys.json
@@ -0,0 +1,4 @@
+{
+ "type": "array",
+ "items": { "$ref": "deploy_key.json" }
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/packages/npm_package_version.json b/spec/fixtures/api/schemas/public_api/v4/packages/npm_package_version.json
index 3e74dc0a1c2..64969d71250 100644
--- a/spec/fixtures/api/schemas/public_api/v4/packages/npm_package_version.json
+++ b/spec/fixtures/api/schemas/public_api/v4/packages/npm_package_version.json
@@ -36,11 +36,11 @@
".{1,}": { "type": "string" }
}
},
- "deprecated": {
- "type": "object",
- "patternProperties": {
- ".{1,}": { "type": "string" }
- }
- }
+ "deprecated": { "type": "string"},
+ "bin": { "type": "string" },
+ "directories": { "type": "array" },
+ "engines": { "type": "object" },
+ "_hasShrinkwrap": { "type": "boolean" },
+ "additionalProperties": true
}
}
diff --git a/spec/fixtures/bulk_imports/gz/milestones.ndjson.gz b/spec/fixtures/bulk_imports/gz/milestones.ndjson.gz
deleted file mode 100644
index f959cd7a0bd..00000000000
--- a/spec/fixtures/bulk_imports/gz/milestones.ndjson.gz
+++ /dev/null
Binary files differ
diff --git a/spec/fixtures/bulk_imports/milestones.ndjson b/spec/fixtures/bulk_imports/milestones.ndjson
deleted file mode 100644
index 40523f276e7..00000000000
--- a/spec/fixtures/bulk_imports/milestones.ndjson
+++ /dev/null
@@ -1,5 +0,0 @@
-{"id":7642,"title":"v4.0","project_id":null,"description":"Et laudantium enim omnis ea reprehenderit iure.","due_date":null,"created_at":"2019-11-20T17:02:14.336Z","updated_at":"2019-11-20T17:02:14.336Z","state":"closed","iid":5,"start_date":null,"group_id":4351}
-{"id":7641,"title":"v3.0","project_id":null,"description":"Et repellat culpa nemo consequatur ut reprehenderit.","due_date":null,"created_at":"2019-11-20T17:02:14.323Z","updated_at":"2019-11-20T17:02:14.323Z","state":"active","iid":4,"start_date":null,"group_id":4351}
-{"id":7640,"title":"v2.0","project_id":null,"description":"Velit cupiditate est neque voluptates iste rem sunt.","due_date":null,"created_at":"2019-11-20T17:02:14.309Z","updated_at":"2019-11-20T17:02:14.309Z","state":"active","iid":3,"start_date":null,"group_id":4351}
-{"id":7639,"title":"v1.0","project_id":null,"description":"Amet velit repellat ut rerum aut cum.","due_date":null,"created_at":"2019-11-20T17:02:14.296Z","updated_at":"2019-11-20T17:02:14.296Z","state":"active","iid":2,"start_date":null,"group_id":4351}
-{"id":7638,"title":"v0.0","project_id":null,"description":"Ea quia asperiores ut modi dolorem sunt non numquam.","due_date":null,"created_at":"2019-11-20T17:02:14.282Z","updated_at":"2019-11-20T17:02:14.282Z","state":"active","iid":1,"start_date":null,"group_id":4351}
diff --git a/spec/fixtures/emails/service_desk_all_quoted.eml b/spec/fixtures/emails/service_desk_all_quoted.eml
new file mode 100644
index 00000000000..102ebf1f30e
--- /dev/null
+++ b/spec/fixtures/emails/service_desk_all_quoted.eml
@@ -0,0 +1,22 @@
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+email-test-project_id-issue-@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: incoming+email-test-project_id-issue-@appmail.adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+Subject: The message subject! @all
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
+
+> This is an empty quote
+> someone did forward this email without
+> adding any new content.
diff --git a/spec/fixtures/emails/service_desk_custom_address_no_key.eml b/spec/fixtures/emails/service_desk_custom_address_no_key.eml
new file mode 100644
index 00000000000..4781e3d4fbd
--- /dev/null
+++ b/spec/fixtures/emails/service_desk_custom_address_no_key.eml
@@ -0,0 +1,27 @@
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <support+project_slug-project_key@example.com>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: support+email-test-project_id-issue-@example.com
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+Subject: The message subject! @all
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
+
+Service desk stuff!
+
+```
+a = b
+```
+
+/label ~label1
+/assign @user1
+/close
diff --git a/spec/fixtures/emails/service_desk_forwarded.eml b/spec/fixtures/emails/service_desk_forwarded.eml
index 56987972808..ab509cf55af 100644
--- a/spec/fixtures/emails/service_desk_forwarded.eml
+++ b/spec/fixtures/emails/service_desk_forwarded.eml
@@ -1,11 +1,11 @@
Delivered-To: incoming+email-test-project_id-issue-@appmail.adventuretime.ooo
-Return-Path: <jake@adventuretime.ooo>
+Return-Path: <jake.g@adventuretime.ooo>
Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+email-test-project_id-issue-@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
Date: Thu, 13 Jun 2013 17:03:48 -0400
-From: Jake the Dog <jake@adventuretime.ooo>
+From: Jake the Dog <jake.g@adventuretime.ooo>
To: support@adventuretime.ooo
Delivered-To: support@adventuretime.ooo
Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
diff --git a/spec/fixtures/error_tracking/browser_event.json b/spec/fixtures/error_tracking/browser_event.json
new file mode 100644
index 00000000000..65918c3dc7a
--- /dev/null
+++ b/spec/fixtures/error_tracking/browser_event.json
@@ -0,0 +1 @@
+{"sdk":{"name":"sentry.javascript.browser","version":"5.7.1","packages":[{"name":"npm:@sentry/browser","version":"5.7.1"}],"integrations":["InboundFilters","FunctionToString","TryCatch","Breadcrumbs","GlobalHandlers","LinkedErrors","UserAgent","Dedupe","ExtraErrorData","ReportingObserver","RewriteFrames","Vue"]},"level":"error","request":{"url":"http://localhost:5444/","headers":{"User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:91.0) Gecko/20100101 Firefox/91.0"}},"event_id":"6a32dc45cd924196930e06aa21b48c8d","platform":"javascript","exception":{"values":[{"type":"TypeError","value":"Cannot read property 'filter' of undefined","mechanism":{"type":"generic","handled":true},"stacktrace":{"frames":[{"colno":34,"in_app":true,"lineno":6395,"filename":"webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js","function":"hydrate"},{"colno":57,"in_app":true,"lineno":6362,"filename":"webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js","function":"hydrate"},{"colno":13,"in_app":true,"lineno":3115,"filename":"webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js","function":"init"},{"colno":10,"in_app":true,"lineno":8399,"filename":"webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js","function":"Vue.prototype.$mount"},{"colno":3,"in_app":true,"lineno":4061,"filename":"webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js","function":"mountComponent"},{"colno":12,"in_app":true,"lineno":4456,"filename":"webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js","function":"Watcher"},{"colno":25,"in_app":true,"lineno":4467,"filename":"webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js","function":"get"},{"colno":10,"in_app":true,"lineno":4048,"filename":"webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js","function":"updateComponent"},{"colno":19,"in_app":true,"lineno":3933,"filename":"webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js","function":"lifecycleMixin/Vue.prototype._update"},{"colno":24,"in_app":true,"lineno":6477,"filename":"webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js","function":"patch"},{"colno":34,"in_app":true,"lineno":6395,"filename":"webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js","function":"hydrate"},{"colno":64,"in_app":true,"lineno":78,"filename":"webpack-internal:///./node_modules/babel-loader/lib/index.js?!./node_modules/vue-loader/lib/index.js?!./pages/index.vue?vue&type=script&lang=js&","function":"data"}]}}]},"environment":"development"} \ No newline at end of file
diff --git a/spec/fixtures/error_tracking/go_parsed_event.json b/spec/fixtures/error_tracking/go_parsed_event.json
new file mode 100644
index 00000000000..9811fc261c0
--- /dev/null
+++ b/spec/fixtures/error_tracking/go_parsed_event.json
@@ -0,0 +1 @@
+{"contexts":{"device":{"arch":"amd64","num_cpu":20},"os":{"name":"linux"},"runtime":{"go_maxprocs":20,"go_numcgocalls":5,"go_numroutines":2,"name":"go","version":"go1.17.1"}},"environment":"Accumulate","event_id":"a6d33282b0d44ed1a5982c48b62e2a4e","level":"error","platform":"go","release":"accumulated@version unknown","sdk":{"name":"sentry.go","version":"0.11.0","integrations":["ContextifyFrames","Environment","IgnoreErrors","Modules"],"packages":[{"name":"sentry-go","version":"0.11.0"}]},"server_name":"Laurelin","user":{},"modules":{"github.com/AccumulateNetwork/accumulated":"(devel)","github.com/AccumulateNetwork/jsonrpc2/v15":"v15.0.0-20210802145948-43d2d974a106","github.com/AndreasBriese/bbloom":"v0.0.0-20190825152654-46b345b51c96","github.com/Workiva/go-datastructures":"v1.0.53","github.com/beorn7/perks":"v1.0.1","github.com/btcsuite/btcd":"v0.22.0-beta","github.com/cespare/xxhash":"v1.1.0","github.com/cespare/xxhash/v2":"v2.1.2","github.com/davecgh/go-spew":"v1.1.2-0.20180830191138-d8f796af33cc","github.com/dgraph-io/badger":"v1.6.2","github.com/dgraph-io/ristretto":"v0.0.4-0.20210122082011-bb5d392ed82d","github.com/dustin/go-humanize":"v1.0.0","github.com/fatih/color":"v1.13.0","github.com/fsnotify/fsnotify":"v1.5.1","github.com/getsentry/sentry-go":"v0.11.0","github.com/go-kit/kit":"v0.11.0","github.com/go-playground/locales":"v0.14.0","github.com/go-playground/universal-translator":"v0.18.0","github.com/go-playground/validator/v10":"v10.9.0","github.com/gogo/protobuf":"v1.3.2","github.com/golang/protobuf":"v1.5.2","github.com/golang/snappy":"v0.0.1","github.com/google/btree":"v1.0.0","github.com/google/orderedcode":"v0.0.1","github.com/google/uuid":"v1.3.0","github.com/gorilla/mux":"v1.8.0","github.com/gorilla/websocket":"v1.4.2","github.com/grpc-ecosystem/go-grpc-middleware":"v1.3.0","github.com/grpc-ecosystem/go-grpc-prometheus":"v1.2.0","github.com/hashicorp/hcl":"v1.0.0","github.com/leodido/go-urn":"v1.2.1","github.com/lib/pq":"v1.10.3","github.com/libp2p/go-buffer-pool":"v0.0.2","github.com/magiconair/properties":"v1.8.5","github.com/mattn/go-colorable":"v0.1.9","github.com/mattn/go-isatty":"v0.0.14","github.com/matttproud/golang_protobuf_extensions":"v1.0.1","github.com/minio/highwayhash":"v1.0.2","github.com/mitchellh/mapstructure":"v1.4.2","github.com/oasisprotocol/curve25519-voi":"v0.0.0-20210609091139-0a56a4bca00b","github.com/pelletier/go-toml":"v1.9.4","github.com/pkg/errors":"v0.9.1","github.com/pmezard/go-difflib":"v1.0.0","github.com/prometheus/client_golang":"v1.11.0","github.com/prometheus/client_model":"v0.2.0","github.com/prometheus/common":"v0.30.0","github.com/prometheus/procfs":"v0.7.3","github.com/rcrowley/go-metrics":"v0.0.0-20200313005456-10cdbea86bc0","github.com/rs/cors":"v1.8.0","github.com/rs/zerolog":"v1.24.0","github.com/spf13/afero":"v1.6.0","github.com/spf13/cast":"v1.4.1","github.com/spf13/cobra":"v1.2.1","github.com/spf13/jwalterweatherman":"v1.1.0","github.com/spf13/pflag":"v1.0.5","github.com/spf13/viper":"v1.8.1","github.com/stretchr/testify":"v1.7.0","github.com/subosito/gotenv":"v1.2.0","github.com/syndtr/goleveldb":"v1.0.1-0.20200815110645-5c35d600f0ca","github.com/tendermint/tendermint":"v0.35.0-rc1","github.com/tendermint/tm-db":"v0.6.4","github.com/ybbus/jsonrpc/v2":"v2.1.6","golang.org/x/crypto":"v0.0.0-20210711020723-a769d52b0f97","golang.org/x/net":"v0.0.0-20210805182204-aaa1db679c0d","golang.org/x/sys":"v0.0.0-20210917161153-d61c044b1678","golang.org/x/term":"v0.0.0-20201126162022-7de9c90e9dd1","golang.org/x/text":"v0.3.7","google.golang.org/genproto":"v0.0.0-20210602131652-f16073e35f0c","google.golang.org/grpc":"v1.40.0","google.golang.org/protobuf":"v1.27.1","gopkg.in/ini.v1":"v1.63.2","gopkg.in/yaml.v2":"v2.4.0","gopkg.in/yaml.v3":"v3.0.0-20210107192922-496545a6307b"},"exception":[{"type":"*errors.errorString","value":"Hello world","stacktrace":{"frames":[{"function":"main","module":"main","abs_path":"SRC/cmd/accumulated/main.go","lineno":37,"pre_context":["func init() {","\tcmdMain.PersistentFlags().StringVarP(&flagMain.WorkDir, \"work-dir\", \"w\", defaultWorkDir, \"Working directory for configuration and data\")","}","","func main() {"],"context_line":"\tcmdMain.Execute()","post_context":["}","","func printUsageAndExit1(cmd *cobra.Command, args []string) {","\tcmd.Usage()","\tos.Exit(1)"],"in_app":true},{"function":"(*Command).Execute","module":"github.com/spf13/cobra","abs_path":"GOPATH/pkg/mod/github.com/spf13/cobra@v1.2.1/command.go","lineno":902,"pre_context":["","// Execute uses the args (os.Args[1:] by default)","// and run through the command tree finding appropriate matches","// for commands and then corresponding flags.","func (c *Command) Execute() error {"],"context_line":"\t_, err := c.ExecuteC()","post_context":["\treturn err","}","","// ExecuteContextC is the same as ExecuteC(), but sets the ctx on the command.","// Retrieve ctx by calling cmd.Context() inside your *Run lifecycle or ValidArgs"],"in_app":true},{"function":"(*Command).ExecuteC","module":"github.com/spf13/cobra","abs_path":"GOPATH/pkg/mod/github.com/spf13/cobra@v1.2.1/command.go","lineno":974,"pre_context":["\t// if context is present on the parent command.","\tif cmd.ctx == nil {","\t\tcmd.ctx = c.ctx","\t}",""],"context_line":"\terr = cmd.execute(flags)","post_context":["\tif err != nil {","\t\t// Always show help if requested, even if SilenceErrors is in","\t\t// effect","\t\tif err == flag.ErrHelp {","\t\t\tcmd.HelpFunc()(cmd, args)"],"in_app":true},{"function":"(*Command).execute","module":"github.com/spf13/cobra","abs_path":"GOPATH/pkg/mod/github.com/spf13/cobra@v1.2.1/command.go","lineno":860,"pre_context":["\tif c.RunE != nil {","\t\tif err := c.RunE(c, argWoFlags); err != nil {","\t\t\treturn err","\t\t}","\t} else {"],"context_line":"\t\tc.Run(c, argWoFlags)","post_context":["\t}","\tif c.PostRunE != nil {","\t\tif err := c.PostRunE(c, argWoFlags); err != nil {","\t\t\treturn err","\t\t}"],"in_app":true},{"function":"runNode","module":"main","abs_path":"SRC/cmd/accumulated/cmd_run.go","lineno":76,"pre_context":["\t\tif err != nil {","\t\t\tfmt.Fprintf(os.Stderr, \"Error: configuring sentry: %v\\n\", err)","\t\t\tos.Exit(1)","\t\t}","\t\tdefer sentry.Flush(2 * time.Second)"],"context_line":"\t\tsentry.CaptureException(errors.New(\"Hello world\"))","post_context":["\t\t// sentry.CaptureMessage(\"Hello world\")","\t\tsentry.Flush(time.Second)","\t}","","\tdbPath := filepath.Join(config.RootDir, \"valacc.db\")"],"in_app":true}]}}],"timestamp":"2021-10-08T19:49:21.932425444-05:00"}
diff --git a/spec/fixtures/error_tracking/python_event.json b/spec/fixtures/error_tracking/python_event.json
new file mode 100644
index 00000000000..4b27cb47e5b
--- /dev/null
+++ b/spec/fixtures/error_tracking/python_event.json
@@ -0,0 +1 @@
+{"level":"error","exception":{"values":[{"module":null,"type":"ZeroDivisionError","value":"division by zero","mechanism":{"type":"django","handled":false},"stacktrace":{"frames":[{"filename":"django/core/handlers/exception.py","abs_path":"/Users/dzaporozhets/.asdf/installs/python/3.8.12/lib/python3.8/site-packages/django/core/handlers/exception.py","function":"inner","module":"django.core.handlers.exception","lineno":47,"pre_context":[" return inner"," else:"," @wraps(get_response)"," def inner(request):"," try:"],"context_line":" response = get_response(request)","post_context":[" except Exception as exc:"," response = response_for_exception(request, exc)"," return response"," return inner",""],"vars":{"request":"\u003cWSGIRequest: GET '/polls/'\u003e","exc":"ZeroDivisionError('division by zero')","get_response":"\u003cbound method BaseHandler._get_response of \u003cdjango.core.handlers.wsgi.WSGIHandler object at 0x10f012550\u003e\u003e"},"in_app":true},{"filename":"django/core/handlers/base.py","abs_path":"/Users/dzaporozhets/.asdf/installs/python/3.8.12/lib/python3.8/site-packages/django/core/handlers/base.py","function":"_get_response","module":"django.core.handlers.base","lineno":181,"pre_context":[" wrapped_callback = self.make_view_atomic(callback)"," # If it is an asynchronous view, run it in a subthread."," if asyncio.iscoroutinefunction(wrapped_callback):"," wrapped_callback = async_to_sync(wrapped_callback)"," try:"],"context_line":" response = wrapped_callback(request, *callback_args, **callback_kwargs)","post_context":[" except Exception as e:"," response = self.process_exception_by_middleware(e, request)"," if response is None:"," raise",""],"vars":{"self":"\u003cdjango.core.handlers.wsgi.WSGIHandler object at 0x10f012550\u003e","request":"\u003cWSGIRequest: GET '/polls/'\u003e","response":"None","callback":"\u003cfunction index at 0x10f7b6820\u003e","callback_args":[],"callback_kwargs":{},"middleware_method":"\u003cfunction CsrfViewMiddleware.process_view at 0x113853a60\u003e","wrapped_callback":"\u003cfunction index at 0x113d41040\u003e"},"in_app":true},{"filename":"polls/views.py","abs_path":"/Users/dzaporozhets/Projects/pysite/polls/views.py","function":"index","module":"polls.views","lineno":15,"pre_context":[" # We recommend adjusting this value in production."," traces_sample_rate=1.0,",")","","def index(request):"],"context_line":" division_by_zero = 1 / 0","post_context":[" return HttpResponse(\"Hello, world. You're at the polls index.\")"],"vars":{"request":"\u003cWSGIRequest: GET '/polls/'\u003e"},"in_app":true}]}}]},"event_id":"dbae4fc6415f408786174a929363d26f","timestamp":"2021-10-07T14:52:18.257544Z","breadcrumbs":{"values":[]},"transaction":"/polls/","contexts":{"trace":{"trace_id":"20b50c065f4f4b5d99862e5ea08b45aa","span_id":"a4ecb3118f7f9f5a","parent_span_id":"af50a83a73a41c28","op":"django.middleware","description":"django.middleware.clickjacking.XFrameOptionsMiddleware.__call__"},"runtime":{"name":"CPython","version":"3.8.12","build":"3.8.12 (default, Oct 6 2021, 13:48:19) \n[Clang 12.0.5 (clang-1205.0.22.9)]"}},"modules":{"urllib3":"1.26.7","sqlparse":"0.4.2","setuptools":"56.0.0","sentry-sdk":"1.4.3","pytz":"2021.3","pip":"21.1.1","django":"3.2.8","certifi":"2021.5.30","asgiref":"3.4.1"},"extra":{"sys.argv":["manage.py","runserver"]},"request":{"url":"http://localhost:8000/polls/","query_string":"","method":"GET","env":{"SERVER_NAME":"1.0.0.127.in-addr.arpa","SERVER_PORT":"8000"},"headers":{"Content-Length":"","Content-Type":"text/plain","Host":"localhost:8000","User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:92.0) Gecko/20100101 Firefox/92.0","Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8","Accept-Language":"en-US,en;q=0.5","Accept-Encoding":"gzip, deflate","Connection":"keep-alive","Cookie":"","Upgrade-Insecure-Requests":"1","Sec-Fetch-Dest":"document","Sec-Fetch-Mode":"navigate","Sec-Fetch-Site":"none","Sec-Fetch-User":"?1","Cache-Control":"max-age=0"}},"environment":"production","server_name":"DZ-GitLab-MBP-15.local","sdk":{"name":"sentry.python","version":"1.4.3","packages":[{"name":"pypi:sentry-sdk","version":"1.4.3"}],"integrations":["argv","atexit","dedupe","django","excepthook","logging","modules","stdlib","threading"]},"platform":"python","_meta":{"request":{"headers":{"Cookie":{"":{"rem":[["!config","x",0,2968]]}}}}}} \ No newline at end of file
diff --git a/spec/fixtures/gitlab/import_export/lightweight_project_export.tar.gz b/spec/fixtures/gitlab/import_export/lightweight_project_export.tar.gz
index e3ec4f603b9..e5f6f195fe5 100644
--- a/spec/fixtures/gitlab/import_export/lightweight_project_export.tar.gz
+++ b/spec/fixtures/gitlab/import_export/lightweight_project_export.tar.gz
Binary files differ
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/project.json b/spec/fixtures/lib/gitlab/import_export/complex/project.json
index fd4c2d55124..95f2ce45b46 100644
--- a/spec/fixtures/lib/gitlab/import_export/complex/project.json
+++ b/spec/fixtures/lib/gitlab/import_export/complex/project.json
@@ -2795,11 +2795,7 @@
"sha": "bb5206fee213d983da88c47f9cf4cc6caf9c66dc",
"message": "Feature conflict added\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-08-06T08:35:52.000+02:00",
- "author_name": "Dmitriy Zaporozhets",
- "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-08-06T08:35:52.000+02:00",
- "committer_name": "Dmitriy Zaporozhets",
- "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
@@ -2815,11 +2811,7 @@
"sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
"message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T10:01:38.000+01:00",
- "author_name": "Dmitriy Zaporozhets",
- "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T10:01:38.000+01:00",
- "committer_name": "Dmitriy Zaporozhets",
- "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
@@ -2835,11 +2827,7 @@
"sha": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d",
"message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:57:31.000+01:00",
- "author_name": "Dmitriy Zaporozhets",
- "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:57:31.000+01:00",
- "committer_name": "Dmitriy Zaporozhets",
- "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
@@ -2855,11 +2843,7 @@
"sha": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9",
"message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:54:21.000+01:00",
- "author_name": "Dmitriy Zaporozhets",
- "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:54:21.000+01:00",
- "committer_name": "Dmitriy Zaporozhets",
- "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
@@ -2875,11 +2859,7 @@
"sha": "d14d6c0abdd253381df51a723d58691b2ee1ab08",
"message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:49:50.000+01:00",
- "author_name": "Dmitriy Zaporozhets",
- "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:49:50.000+01:00",
- "committer_name": "Dmitriy Zaporozhets",
- "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
@@ -2895,11 +2875,7 @@
"sha": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8",
"message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:48:32.000+01:00",
- "author_name": "Dmitriy Zaporozhets",
- "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:48:32.000+01:00",
- "committer_name": "Dmitriy Zaporozhets",
- "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
@@ -3291,11 +3267,7 @@
"relative_order": 0,
"message": "Feature added\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:26:01.000+01:00",
- "author_name": "Dmitriy Zaporozhets",
- "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:26:01.000+01:00",
- "committer_name": "Dmitriy Zaporozhets",
- "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
@@ -3562,11 +3534,7 @@
"sha": "94b8d581c48d894b86661718582fecbc5e3ed2eb",
"message": "fixes #10\n",
"authored_date": "2016-01-19T13:22:56.000+01:00",
- "author_name": "James Lopez",
- "author_email": "james@jameslopez.es",
"committed_date": "2016-01-19T13:22:56.000+01:00",
- "committer_name": "James Lopez",
- "committer_email": "james@jameslopez.es",
"commit_author": {
"name": "James Lopez",
"email": "james@jameslopez.es"
@@ -3833,11 +3801,7 @@
"sha": "ddd4ff416a931589c695eb4f5b23f844426f6928",
"message": "fixes #10\n",
"authored_date": "2016-01-19T14:14:43.000+01:00",
- "author_name": "James Lopez",
- "author_email": "james@jameslopez.es",
"committed_date": "2016-01-19T14:14:43.000+01:00",
- "committer_name": "James Lopez",
- "committer_email": "james@jameslopez.es",
"commit_author": {
"name": "James Lopez",
"email": "james@jameslopez.es"
@@ -3853,11 +3817,7 @@
"sha": "be93687618e4b132087f430a4d8fc3a609c9b77c",
"message": "Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6",
"authored_date": "2015-12-07T12:52:12.000+01:00",
- "author_name": "Marin Jankovski",
- "author_email": "marin@gitlab.com",
"committed_date": "2015-12-07T12:52:12.000+01:00",
- "committer_name": "Marin Jankovski",
- "committer_email": "marin@gitlab.com",
"commit_author": {
"name": "Marin Jankovski",
"email": "marin@gitlab.com"
@@ -3873,11 +3833,7 @@
"sha": "048721d90c449b244b7b4c53a9186b04330174ec",
"message": "LFS object pointer.\n",
"authored_date": "2015-12-07T11:54:28.000+01:00",
- "author_name": "Marin Jankovski",
- "author_email": "maxlazio@gmail.com",
"committed_date": "2015-12-07T11:54:28.000+01:00",
- "committer_name": "Marin Jankovski",
- "committer_email": "maxlazio@gmail.com",
"commit_author": {
"name": "Marin Jankovski",
"email": "maxlazio@gmail.com"
@@ -3893,11 +3849,7 @@
"sha": "5f923865dde3436854e9ceb9cdb7815618d4e849",
"message": "GitLab currently doesn't support patches that involve a merge commit: add a commit here\n",
"authored_date": "2015-11-13T16:27:12.000+01:00",
- "author_name": "Stan Hu",
- "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T16:27:12.000+01:00",
- "committer_name": "Stan Hu",
- "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@@ -3913,11 +3865,7 @@
"sha": "d2d430676773caa88cdaf7c55944073b2fd5561a",
"message": "Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5",
"authored_date": "2015-11-13T08:50:17.000+01:00",
- "author_name": "Stan Hu",
- "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T08:50:17.000+01:00",
- "committer_name": "Stan Hu",
- "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@@ -3933,11 +3881,7 @@
"sha": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73",
"message": "Add GitLab SVG\n",
"authored_date": "2015-11-13T08:39:43.000+01:00",
- "author_name": "Stan Hu",
- "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T08:39:43.000+01:00",
- "committer_name": "Stan Hu",
- "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@@ -3953,11 +3897,7 @@
"sha": "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
"message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4",
"authored_date": "2015-11-13T07:21:40.000+01:00",
- "author_name": "Stan Hu",
- "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T07:21:40.000+01:00",
- "committer_name": "Stan Hu",
- "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@@ -3973,11 +3913,7 @@
"sha": "66eceea0db202bb39c4e445e8ca28689645366c5",
"message": "add spaces in whitespace file\n",
"authored_date": "2015-11-13T06:01:27.000+01:00",
- "author_name": "윤민식",
- "author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T06:01:27.000+01:00",
- "committer_name": "윤민식",
- "committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
@@ -3993,11 +3929,7 @@
"sha": "08f22f255f082689c0d7d39d19205085311542bc",
"message": "remove empty file.(beacase git ignore empty file)\nadd whitespace test file.\n",
"authored_date": "2015-11-13T06:00:16.000+01:00",
- "author_name": "윤민식",
- "author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T06:00:16.000+01:00",
- "committer_name": "윤민식",
- "committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
@@ -4013,11 +3945,7 @@
"sha": "19e2e9b4ef76b422ce1154af39a91323ccc57434",
"message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3",
"authored_date": "2015-11-13T05:23:14.000+01:00",
- "author_name": "Stan Hu",
- "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T05:23:14.000+01:00",
- "committer_name": "Stan Hu",
- "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@@ -4033,11 +3961,7 @@
"sha": "c642fe9b8b9f28f9225d7ea953fe14e74748d53b",
"message": "add whitespace in empty\n",
"authored_date": "2015-11-13T05:08:45.000+01:00",
- "author_name": "윤민식",
- "author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T05:08:45.000+01:00",
- "committer_name": "윤민식",
- "committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
@@ -4053,11 +3977,7 @@
"sha": "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0",
"message": "add empty file\n",
"authored_date": "2015-11-13T05:08:04.000+01:00",
- "author_name": "윤민식",
- "author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T05:08:04.000+01:00",
- "committer_name": "윤민식",
- "committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
@@ -4073,11 +3993,7 @@
"sha": "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
"message": "Add ISO-8859 test file\n",
"authored_date": "2015-08-25T17:53:12.000+02:00",
- "author_name": "Stan Hu",
- "author_email": "stanhu@packetzoom.com",
"committed_date": "2015-08-25T17:53:12.000+02:00",
- "committer_name": "Stan Hu",
- "committer_email": "stanhu@packetzoom.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@packetzoom.com"
@@ -4093,11 +4009,7 @@
"sha": "e56497bb5f03a90a51293fc6d516788730953899",
"message": "Merge branch 'tree_helper_spec' into 'master'\n\nAdd directory structure for tree_helper spec\n\nThis directory structure is needed for a testing the method flatten_tree(tree) in the TreeHelper module\n\nSee [merge request #275](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/275#note_732774)\n\nSee merge request !2\n",
"authored_date": "2015-01-10T22:23:29.000+01:00",
- "author_name": "Sytse Sijbrandij",
- "author_email": "sytse@gitlab.com",
"committed_date": "2015-01-10T22:23:29.000+01:00",
- "committer_name": "Sytse Sijbrandij",
- "committer_email": "sytse@gitlab.com",
"commit_author": {
"name": "Sytse Sijbrandij",
"email": "sytse@gitlab.com"
@@ -4113,11 +4025,7 @@
"sha": "4cd80ccab63c82b4bad16faa5193fbd2aa06df40",
"message": "add directory structure for tree_helper spec\n",
"authored_date": "2015-01-10T21:28:18.000+01:00",
- "author_name": "marmis85",
- "author_email": "marmis85@gmail.com",
"committed_date": "2015-01-10T21:28:18.000+01:00",
- "committer_name": "marmis85",
- "committer_email": "marmis85@gmail.com",
"commit_author": {
"name": "marmis85",
"email": "marmis85@gmail.com"
@@ -4133,11 +4041,7 @@
"sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
"message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T10:01:38.000+01:00",
- "author_name": "Dmitriy Zaporozhets",
- "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T10:01:38.000+01:00",
- "committer_name": "Dmitriy Zaporozhets",
- "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
@@ -4153,11 +4057,7 @@
"sha": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d",
"message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:57:31.000+01:00",
- "author_name": "Dmitriy Zaporozhets",
- "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:57:31.000+01:00",
- "committer_name": "Dmitriy Zaporozhets",
- "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
@@ -4173,11 +4073,7 @@
"sha": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9",
"message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:54:21.000+01:00",
- "author_name": "Dmitriy Zaporozhets",
- "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:54:21.000+01:00",
- "committer_name": "Dmitriy Zaporozhets",
- "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
@@ -4193,11 +4089,7 @@
"sha": "d14d6c0abdd253381df51a723d58691b2ee1ab08",
"message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:49:50.000+01:00",
- "author_name": "Dmitriy Zaporozhets",
- "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:49:50.000+01:00",
- "committer_name": "Dmitriy Zaporozhets",
- "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
@@ -4213,11 +4105,7 @@
"sha": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8",
"message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:48:32.000+01:00",
- "author_name": "Dmitriy Zaporozhets",
- "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:48:32.000+01:00",
- "committer_name": "Dmitriy Zaporozhets",
- "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
@@ -4678,11 +4566,7 @@
"sha": "0bfedc29d30280c7e8564e19f654584b459e5868",
"message": "fixes #10\n",
"authored_date": "2016-01-19T15:25:23.000+01:00",
- "author_name": "James Lopez",
- "author_email": "james@jameslopez.es",
"committed_date": "2016-01-19T15:25:23.000+01:00",
- "committer_name": "James Lopez",
- "committer_email": "james@jameslopez.es",
"commit_author": {
"name": "James Lopez",
"email": "james@jameslopez.es"
@@ -4698,11 +4582,7 @@
"sha": "be93687618e4b132087f430a4d8fc3a609c9b77c",
"message": "Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6",
"authored_date": "2015-12-07T12:52:12.000+01:00",
- "author_name": "Marin Jankovski",
- "author_email": "marin@gitlab.com",
"committed_date": "2015-12-07T12:52:12.000+01:00",
- "committer_name": "Marin Jankovski",
- "committer_email": "marin@gitlab.com",
"commit_author": {
"name": "Marin Jankovski",
"email": "marin@gitlab.com"
@@ -4718,11 +4598,7 @@
"sha": "048721d90c449b244b7b4c53a9186b04330174ec",
"message": "LFS object pointer.\n",
"authored_date": "2015-12-07T11:54:28.000+01:00",
- "author_name": "Marin Jankovski",
- "author_email": "maxlazio@gmail.com",
"committed_date": "2015-12-07T11:54:28.000+01:00",
- "committer_name": "Marin Jankovski",
- "committer_email": "maxlazio@gmail.com",
"commit_author": {
"name": "Marin Jankovski",
"email": "maxlazio@gmail.com"
@@ -4738,11 +4614,7 @@
"sha": "5f923865dde3436854e9ceb9cdb7815618d4e849",
"message": "GitLab currently doesn't support patches that involve a merge commit: add a commit here\n",
"authored_date": "2015-11-13T16:27:12.000+01:00",
- "author_name": "Stan Hu",
- "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T16:27:12.000+01:00",
- "committer_name": "Stan Hu",
- "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@@ -4758,11 +4630,7 @@
"sha": "d2d430676773caa88cdaf7c55944073b2fd5561a",
"message": "Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5",
"authored_date": "2015-11-13T08:50:17.000+01:00",
- "author_name": "Stan Hu",
- "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T08:50:17.000+01:00",
- "committer_name": "Stan Hu",
- "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@@ -4778,11 +4646,7 @@
"sha": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73",
"message": "Add GitLab SVG\n",
"authored_date": "2015-11-13T08:39:43.000+01:00",
- "author_name": "Stan Hu",
- "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T08:39:43.000+01:00",
- "committer_name": "Stan Hu",
- "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@@ -4798,11 +4662,7 @@
"sha": "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
"message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4",
"authored_date": "2015-11-13T07:21:40.000+01:00",
- "author_name": "Stan Hu",
- "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T07:21:40.000+01:00",
- "committer_name": "Stan Hu",
- "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@@ -4818,11 +4678,7 @@
"sha": "66eceea0db202bb39c4e445e8ca28689645366c5",
"message": "add spaces in whitespace file\n",
"authored_date": "2015-11-13T06:01:27.000+01:00",
- "author_name": "윤민식",
- "author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T06:01:27.000+01:00",
- "committer_name": "윤민식",
- "committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
@@ -4838,11 +4694,7 @@
"sha": "08f22f255f082689c0d7d39d19205085311542bc",
"message": "remove empty file.(beacase git ignore empty file)\nadd whitespace test file.\n",
"authored_date": "2015-11-13T06:00:16.000+01:00",
- "author_name": "윤민식",
- "author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T06:00:16.000+01:00",
- "committer_name": "윤민식",
- "committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
@@ -4858,11 +4710,7 @@
"sha": "19e2e9b4ef76b422ce1154af39a91323ccc57434",
"message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3",
"authored_date": "2015-11-13T05:23:14.000+01:00",
- "author_name": "Stan Hu",
- "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T05:23:14.000+01:00",
- "committer_name": "Stan Hu",
- "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@@ -4878,11 +4726,7 @@
"sha": "c642fe9b8b9f28f9225d7ea953fe14e74748d53b",
"message": "add whitespace in empty\n",
"authored_date": "2015-11-13T05:08:45.000+01:00",
- "author_name": "윤민식",
- "author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T05:08:45.000+01:00",
- "committer_name": "윤민식",
- "committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
@@ -4898,11 +4742,7 @@
"sha": "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0",
"message": "add empty file\n",
"authored_date": "2015-11-13T05:08:04.000+01:00",
- "author_name": "윤민식",
- "author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T05:08:04.000+01:00",
- "committer_name": "윤민식",
- "committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
@@ -4918,11 +4758,7 @@
"sha": "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
"message": "Add ISO-8859 test file\n",
"authored_date": "2015-08-25T17:53:12.000+02:00",
- "author_name": "Stan Hu",
- "author_email": "stanhu@packetzoom.com",
"committed_date": "2015-08-25T17:53:12.000+02:00",
- "committer_name": "Stan Hu",
- "committer_email": "stanhu@packetzoom.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@packetzoom.com"
@@ -4938,11 +4774,7 @@
"sha": "e56497bb5f03a90a51293fc6d516788730953899",
"message": "Merge branch 'tree_helper_spec' into 'master'\n\nAdd directory structure for tree_helper spec\n\nThis directory structure is needed for a testing the method flatten_tree(tree) in the TreeHelper module\n\nSee [merge request #275](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/275#note_732774)\n\nSee merge request !2\n",
"authored_date": "2015-01-10T22:23:29.000+01:00",
- "author_name": "Sytse Sijbrandij",
- "author_email": "sytse@gitlab.com",
"committed_date": "2015-01-10T22:23:29.000+01:00",
- "committer_name": "Sytse Sijbrandij",
- "committer_email": "sytse@gitlab.com",
"commit_author": {
"name": "Sytse Sijbrandij",
"email": "sytse@gitlab.com"
@@ -4958,11 +4790,7 @@
"sha": "4cd80ccab63c82b4bad16faa5193fbd2aa06df40",
"message": "add directory structure for tree_helper spec\n",
"authored_date": "2015-01-10T21:28:18.000+01:00",
- "author_name": "marmis85",
- "author_email": "marmis85@gmail.com",
"committed_date": "2015-01-10T21:28:18.000+01:00",
- "committer_name": "marmis85",
- "committer_email": "marmis85@gmail.com",
"commit_author": {
"name": "marmis85",
"email": "marmis85@gmail.com"
@@ -5307,11 +5135,7 @@
"sha": "97a0df9696e2aebf10c31b3016f40214e0e8f243",
"message": "fixes #10\n",
"authored_date": "2016-01-19T14:08:21.000+01:00",
- "author_name": "James Lopez",
- "author_email": "james@jameslopez.es",
"committed_date": "2016-01-19T14:08:21.000+01:00",
- "committer_name": "James Lopez",
- "committer_email": "james@jameslopez.es",
"commit_author": {
"name": "James Lopez",
"email": "james@jameslopez.es"
@@ -5327,11 +5151,7 @@
"sha": "be93687618e4b132087f430a4d8fc3a609c9b77c",
"message": "Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6",
"authored_date": "2015-12-07T12:52:12.000+01:00",
- "author_name": "Marin Jankovski",
- "author_email": "marin@gitlab.com",
"committed_date": "2015-12-07T12:52:12.000+01:00",
- "committer_name": "Marin Jankovski",
- "committer_email": "marin@gitlab.com",
"commit_author": {
"name": "Marin Jankovski",
"email": "marin@gitlab.com"
@@ -5347,11 +5167,7 @@
"sha": "048721d90c449b244b7b4c53a9186b04330174ec",
"message": "LFS object pointer.\n",
"authored_date": "2015-12-07T11:54:28.000+01:00",
- "author_name": "Marin Jankovski",
- "author_email": "maxlazio@gmail.com",
"committed_date": "2015-12-07T11:54:28.000+01:00",
- "committer_name": "Marin Jankovski",
- "committer_email": "maxlazio@gmail.com",
"commit_author": {
"name": "Marin Jankovski",
"email": "maxlazio@gmail.com"
@@ -5367,11 +5183,7 @@
"sha": "5f923865dde3436854e9ceb9cdb7815618d4e849",
"message": "GitLab currently doesn't support patches that involve a merge commit: add a commit here\n",
"authored_date": "2015-11-13T16:27:12.000+01:00",
- "author_name": "Stan Hu",
- "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T16:27:12.000+01:00",
- "committer_name": "Stan Hu",
- "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@@ -5387,11 +5199,7 @@
"sha": "d2d430676773caa88cdaf7c55944073b2fd5561a",
"message": "Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5",
"authored_date": "2015-11-13T08:50:17.000+01:00",
- "author_name": "Stan Hu",
- "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T08:50:17.000+01:00",
- "committer_name": "Stan Hu",
- "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@@ -5407,11 +5215,7 @@
"sha": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73",
"message": "Add GitLab SVG\n",
"authored_date": "2015-11-13T08:39:43.000+01:00",
- "author_name": "Stan Hu",
- "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T08:39:43.000+01:00",
- "committer_name": "Stan Hu",
- "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@@ -5427,11 +5231,7 @@
"sha": "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
"message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4",
"authored_date": "2015-11-13T07:21:40.000+01:00",
- "author_name": "Stan Hu",
- "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T07:21:40.000+01:00",
- "committer_name": "Stan Hu",
- "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@@ -5447,11 +5247,7 @@
"sha": "66eceea0db202bb39c4e445e8ca28689645366c5",
"message": "add spaces in whitespace file\n",
"authored_date": "2015-11-13T06:01:27.000+01:00",
- "author_name": "윤민식",
- "author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T06:01:27.000+01:00",
- "committer_name": "윤민식",
- "committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
@@ -5467,11 +5263,7 @@
"sha": "08f22f255f082689c0d7d39d19205085311542bc",
"message": "remove empty file.(beacase git ignore empty file)\nadd whitespace test file.\n",
"authored_date": "2015-11-13T06:00:16.000+01:00",
- "author_name": "윤민식",
- "author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T06:00:16.000+01:00",
- "committer_name": "윤민식",
- "committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
@@ -5487,11 +5279,7 @@
"sha": "19e2e9b4ef76b422ce1154af39a91323ccc57434",
"message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3",
"authored_date": "2015-11-13T05:23:14.000+01:00",
- "author_name": "Stan Hu",
- "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T05:23:14.000+01:00",
- "committer_name": "Stan Hu",
- "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@@ -5507,11 +5295,7 @@
"sha": "c642fe9b8b9f28f9225d7ea953fe14e74748d53b",
"message": "add whitespace in empty\n",
"authored_date": "2015-11-13T05:08:45.000+01:00",
- "author_name": "윤민식",
- "author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T05:08:45.000+01:00",
- "committer_name": "윤민식",
- "committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
@@ -5527,11 +5311,7 @@
"sha": "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0",
"message": "add empty file\n",
"authored_date": "2015-11-13T05:08:04.000+01:00",
- "author_name": "윤민식",
- "author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T05:08:04.000+01:00",
- "committer_name": "윤민식",
- "committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
@@ -5547,11 +5327,7 @@
"sha": "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
"message": "Add ISO-8859 test file\n",
"authored_date": "2015-08-25T17:53:12.000+02:00",
- "author_name": "Stan Hu",
- "author_email": "stanhu@packetzoom.com",
"committed_date": "2015-08-25T17:53:12.000+02:00",
- "committer_name": "Stan Hu",
- "committer_email": "stanhu@packetzoom.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@packetzoom.com"
@@ -6119,11 +5895,7 @@
"sha": "f998ac87ac9244f15e9c15109a6f4e62a54b779d",
"message": "fixes #10\n",
"authored_date": "2016-01-19T14:43:23.000+01:00",
- "author_name": "James Lopez",
- "author_email": "james@jameslopez.es",
"committed_date": "2016-01-19T14:43:23.000+01:00",
- "committer_name": "James Lopez",
- "committer_email": "james@jameslopez.es",
"commit_author": {
"name": "James Lopez",
"email": "james@jameslopez.es"
@@ -6139,11 +5911,7 @@
"sha": "be93687618e4b132087f430a4d8fc3a609c9b77c",
"message": "Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6",
"authored_date": "2015-12-07T12:52:12.000+01:00",
- "author_name": "Marin Jankovski",
- "author_email": "marin@gitlab.com",
"committed_date": "2015-12-07T12:52:12.000+01:00",
- "committer_name": "Marin Jankovski",
- "committer_email": "marin@gitlab.com",
"commit_author": {
"name": "Marin Jankovski",
"email": "marin@gitlab.com"
@@ -6159,11 +5927,7 @@
"sha": "048721d90c449b244b7b4c53a9186b04330174ec",
"message": "LFS object pointer.\n",
"authored_date": "2015-12-07T11:54:28.000+01:00",
- "author_name": "Marin Jankovski",
- "author_email": "maxlazio@gmail.com",
"committed_date": "2015-12-07T11:54:28.000+01:00",
- "committer_name": "Marin Jankovski",
- "committer_email": "maxlazio@gmail.com",
"commit_author": {
"name": "Marin Jankovski",
"email": "maxlazio@gmail.com"
@@ -6179,11 +5943,7 @@
"sha": "5f923865dde3436854e9ceb9cdb7815618d4e849",
"message": "GitLab currently doesn't support patches that involve a merge commit: add a commit here\n",
"authored_date": "2015-11-13T16:27:12.000+01:00",
- "author_name": "Stan Hu",
- "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T16:27:12.000+01:00",
- "committer_name": "Stan Hu",
- "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@@ -6199,11 +5959,7 @@
"sha": "d2d430676773caa88cdaf7c55944073b2fd5561a",
"message": "Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5",
"authored_date": "2015-11-13T08:50:17.000+01:00",
- "author_name": "Stan Hu",
- "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T08:50:17.000+01:00",
- "committer_name": "Stan Hu",
- "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@@ -6219,11 +5975,7 @@
"sha": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73",
"message": "Add GitLab SVG\n",
"authored_date": "2015-11-13T08:39:43.000+01:00",
- "author_name": "Stan Hu",
- "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T08:39:43.000+01:00",
- "committer_name": "Stan Hu",
- "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@@ -6239,11 +5991,7 @@
"sha": "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
"message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4",
"authored_date": "2015-11-13T07:21:40.000+01:00",
- "author_name": "Stan Hu",
- "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T07:21:40.000+01:00",
- "committer_name": "Stan Hu",
- "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@@ -6259,11 +6007,7 @@
"sha": "66eceea0db202bb39c4e445e8ca28689645366c5",
"message": "add spaces in whitespace file\n",
"authored_date": "2015-11-13T06:01:27.000+01:00",
- "author_name": "윤민식",
- "author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T06:01:27.000+01:00",
- "committer_name": "윤민식",
- "committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
@@ -6279,11 +6023,7 @@
"sha": "08f22f255f082689c0d7d39d19205085311542bc",
"message": "remove empty file.(beacase git ignore empty file)\nadd whitespace test file.\n",
"authored_date": "2015-11-13T06:00:16.000+01:00",
- "author_name": "윤민식",
- "author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T06:00:16.000+01:00",
- "committer_name": "윤민식",
- "committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
@@ -6299,11 +6039,7 @@
"sha": "19e2e9b4ef76b422ce1154af39a91323ccc57434",
"message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3",
"authored_date": "2015-11-13T05:23:14.000+01:00",
- "author_name": "Stan Hu",
- "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T05:23:14.000+01:00",
- "committer_name": "Stan Hu",
- "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@@ -6319,11 +6055,7 @@
"sha": "c642fe9b8b9f28f9225d7ea953fe14e74748d53b",
"message": "add whitespace in empty\n",
"authored_date": "2015-11-13T05:08:45.000+01:00",
- "author_name": "윤민식",
- "author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T05:08:45.000+01:00",
- "committer_name": "윤민식",
- "committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
@@ -6339,11 +6071,7 @@
"sha": "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0",
"message": "add empty file\n",
"authored_date": "2015-11-13T05:08:04.000+01:00",
- "author_name": "윤민식",
- "author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T05:08:04.000+01:00",
- "committer_name": "윤민식",
- "committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
@@ -6359,11 +6087,7 @@
"sha": "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
"message": "Add ISO-8859 test file\n",
"authored_date": "2015-08-25T17:53:12.000+02:00",
- "author_name": "Stan Hu",
- "author_email": "stanhu@packetzoom.com",
"committed_date": "2015-08-25T17:53:12.000+02:00",
- "committer_name": "Stan Hu",
- "committer_email": "stanhu@packetzoom.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@packetzoom.com"
@@ -6379,11 +6103,7 @@
"sha": "e56497bb5f03a90a51293fc6d516788730953899",
"message": "Merge branch 'tree_helper_spec' into 'master'\n\nAdd directory structure for tree_helper spec\n\nThis directory structure is needed for a testing the method flatten_tree(tree) in the TreeHelper module\n\nSee [merge request #275](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/275#note_732774)\n\nSee merge request !2\n",
"authored_date": "2015-01-10T22:23:29.000+01:00",
- "author_name": "Sytse Sijbrandij",
- "author_email": "sytse@gitlab.com",
"committed_date": "2015-01-10T22:23:29.000+01:00",
- "committer_name": "Sytse Sijbrandij",
- "committer_email": "sytse@gitlab.com",
"commit_author": {
"name": "Sytse Sijbrandij",
"email": "sytse@gitlab.com"
@@ -6399,11 +6119,7 @@
"sha": "4cd80ccab63c82b4bad16faa5193fbd2aa06df40",
"message": "add directory structure for tree_helper spec\n",
"authored_date": "2015-01-10T21:28:18.000+01:00",
- "author_name": "marmis85",
- "author_email": "marmis85@gmail.com",
"committed_date": "2015-01-10T21:28:18.000+01:00",
- "committer_name": "marmis85",
- "committer_email": "marmis85@gmail.com",
"commit_author": {
"name": "marmis85",
"email": "marmis85@gmail.com"
@@ -6419,11 +6135,7 @@
"sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
"message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T10:01:38.000+01:00",
- "author_name": "Dmitriy Zaporozhets",
- "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T10:01:38.000+01:00",
- "committer_name": "Dmitriy Zaporozhets",
- "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
@@ -6439,11 +6151,7 @@
"sha": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d",
"message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:57:31.000+01:00",
- "author_name": "Dmitriy Zaporozhets",
- "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:57:31.000+01:00",
- "committer_name": "Dmitriy Zaporozhets",
- "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
@@ -6459,11 +6167,7 @@
"sha": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9",
"message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:54:21.000+01:00",
- "author_name": "Dmitriy Zaporozhets",
- "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:54:21.000+01:00",
- "committer_name": "Dmitriy Zaporozhets",
- "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
@@ -6479,11 +6183,7 @@
"sha": "d14d6c0abdd253381df51a723d58691b2ee1ab08",
"message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:49:50.000+01:00",
- "author_name": "Dmitriy Zaporozhets",
- "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:49:50.000+01:00",
- "committer_name": "Dmitriy Zaporozhets",
- "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
@@ -6499,11 +6199,7 @@
"sha": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8",
"message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:48:32.000+01:00",
- "author_name": "Dmitriy Zaporozhets",
- "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:48:32.000+01:00",
- "committer_name": "Dmitriy Zaporozhets",
- "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
@@ -6952,11 +6648,7 @@
"sha": "a4e5dfebf42e34596526acb8611bc7ed80e4eb3f",
"message": "fixes #10\n",
"authored_date": "2016-01-19T15:44:02.000+01:00",
- "author_name": "James Lopez",
- "author_email": "james@jameslopez.es",
"committed_date": "2016-01-19T15:44:02.000+01:00",
- "committer_name": "James Lopez",
- "committer_email": "james@jameslopez.es",
"commit_author": {
"name": "James Lopez",
"email": "james@jameslopez.es"
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/merge_requests.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/merge_requests.ndjson
index 741360c0b8e..16e45509a1b 100644
--- a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/merge_requests.ndjson
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/merge_requests.ndjson
@@ -1,9 +1,9 @@
-{"id":27,"target_branch":"feature","source_branch":"feature_conflict","source_project_id":2147483547,"author_id":1,"assignee_id":null,"title":"MR1","created_at":"2016-06-14T15:02:36.568Z","updated_at":"2016-06-14T15:02:56.815Z","state":"opened","merge_status":"unchecked","target_project_id":5,"iid":9,"description":null,"position":0,"updated_by_id":null,"merge_error":null,"diff_head_sha":"HEAD","source_branch_sha":"ABCD","target_branch_sha":"DCBA","merge_params":{"force_remove_source_branch":null},"merge_when_pipeline_succeeds":true,"merge_user_id":null,"merge_commit_sha":null,"notes":[{"id":669,"note":"added 3 commits\n\n<ul><li>16ea4e20...074a2a32 - 2 commits from branch <code>master</code></li><li>ca223a02 - readme: fix typos</li></ul>\n\n[Compare with previous version](/group/project/merge_requests/1/diffs?diff_id=1189&start_sha=16ea4e207fb258fe4e9c73185a725207c9a4f3e1)","noteable_type":"MergeRequest","author_id":26,"created_at":"2020-03-28T12:47:33.461Z","updated_at":"2020-03-28T12:47:33.461Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"system":true,"st_diff":null,"updated_by_id":null,"position":null,"original_position":null,"resolved_at":null,"resolved_by_id":null,"discussion_id":null,"change_position":null,"resolved_by_push":null,"confidential":null,"type":null,"author":{"name":"User 4"},"award_emoji":[],"system_note_metadata":{"id":4789,"commit_count":3,"action":"commit","created_at":"2020-03-28T12:47:33.461Z","updated_at":"2020-03-28T12:47:33.461Z"},"events":[],"suggestions":[]},{"id":670,"note":"unmarked as a **Work In Progress**","noteable_type":"MergeRequest","author_id":26,"created_at":"2020-03-28T12:48:36.951Z","updated_at":"2020-03-28T12:48:36.951Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"system":true,"st_diff":null,"updated_by_id":null,"position":null,"original_position":null,"resolved_at":null,"resolved_by_id":null,"discussion_id":null,"change_position":null,"resolved_by_push":null,"confidential":null,"type":null,"author":{"name":"User 4"},"award_emoji":[],"system_note_metadata":{"id":4790,"commit_count":null,"action":"title","created_at":"2020-03-28T12:48:36.951Z","updated_at":"2020-03-28T12:48:36.951Z"},"events":[],"suggestions":[]},{"id":671,"note":"Sit voluptatibus eveniet architecto quidem.","note_html":"<p>something else entirely</p>","cached_markdown_version":917504,"noteable_type":"MergeRequest","author_id":26,"created_at":"2016-06-14T15:02:56.632Z","updated_at":"2016-06-14T15:02:56.632Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[],"award_emoji":[{"id":1,"name":"tada","user_id":1,"awardable_type":"Note","awardable_id":1,"created_at":"2019-11-05T15:37:21.287Z","updated_at":"2019-11-05T15:37:21.287Z"}]},{"id":672,"note":"Odio maxime ratione voluptatibus sed.","noteable_type":"MergeRequest","author_id":25,"created_at":"2016-06-14T15:02:56.656Z","updated_at":"2016-06-14T15:02:56.656Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":673,"note":"Et deserunt et omnis nihil excepturi accusantium.","noteable_type":"MergeRequest","author_id":22,"created_at":"2016-06-14T15:02:56.679Z","updated_at":"2016-06-14T15:02:56.679Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":674,"note":"Saepe asperiores exercitationem non dignissimos laborum reiciendis et ipsum.","noteable_type":"MergeRequest","author_id":20,"created_at":"2016-06-14T15:02:56.700Z","updated_at":"2016-06-14T15:02:56.700Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[],"suggestions":[{"id":1,"note_id":674,"relative_order":0,"applied":false,"commit_id":null,"from_content":"Original line\n","to_content":"New line\n","lines_above":0,"lines_below":0,"outdated":false}]},{"id":675,"note":"Numquam est at dolor quo et sed eligendi similique.","noteable_type":"MergeRequest","author_id":16,"created_at":"2016-06-14T15:02:56.720Z","updated_at":"2016-06-14T15:02:56.720Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":676,"note":"Et perferendis aliquam sunt nisi labore delectus.","noteable_type":"MergeRequest","author_id":15,"created_at":"2016-06-14T15:02:56.742Z","updated_at":"2016-06-14T15:02:56.742Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":677,"note":"Aut ex rerum et in.","noteable_type":"MergeRequest","author_id":6,"created_at":"2016-06-14T15:02:56.791Z","updated_at":"2016-06-14T15:02:56.791Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":678,"note":"Dolor laborum earum ut exercitationem.","noteable_type":"MergeRequest","author_id":1,"created_at":"2016-06-14T15:02:56.814Z","updated_at":"2016-06-14T15:02:56.814Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"resource_label_events":[{"id":243,"action":"add","issue_id":null,"merge_request_id":27,"label_id":null,"user_id":1,"created_at":"2018-08-28T08:24:00.494Z"}],"merge_request_diff":{"id":27,"state":"collected","merge_request_diff_commits":[{"merge_request_diff_id":27,"relative_order":0,"sha":"bb5206fee213d983da88c47f9cf4cc6caf9c66dc","message":"Feature conflict added\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-08-06T08:35:52.000+02:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-08-06T08:35:52.000+02:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":27,"relative_order":1,"sha":"5937ac0a7beb003549fc5fd26fc247adbce4a52e","message":"Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T10:01:38.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T10:01:38.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":27,"relative_order":2,"sha":"570e7b2abdd848b95f2f578043fc23bd6f6fd24d","message":"Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:57:31.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T09:57:31.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":27,"relative_order":3,"sha":"6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9","message":"More submodules\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:54:21.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T09:54:21.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":27,"relative_order":4,"sha":"d14d6c0abdd253381df51a723d58691b2ee1ab08","message":"Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:49:50.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T09:49:50.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":27,"relative_order":5,"sha":"c1acaa58bbcbc3eafe538cb8274ba387047b69f8","message":"Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:48:32.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T09:48:32.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}}],"merge_request_diff_files":[{"merge_request_diff_id":27,"relative_order":0,"utf8_diff":"Binary files a/.DS_Store and /dev/null differ\n","new_path":".DS_Store","old_path":".DS_Store","a_mode":"100644","b_mode":"0","new_file":false,"renamed_file":false,"deleted_file":true,"too_large":false},{"merge_request_diff_id":27,"relative_order":1,"utf8_diff":"--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n","new_path":".gitignore","old_path":".gitignore","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":27,"relative_order":2,"utf8_diff":"--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n","new_path":".gitmodules","old_path":".gitmodules","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":27,"relative_order":3,"utf8_diff":"Binary files a/files/.DS_Store and /dev/null differ\n","new_path":"files/.DS_Store","old_path":"files/.DS_Store","a_mode":"100644","b_mode":"0","new_file":false,"renamed_file":false,"deleted_file":true,"too_large":false},{"merge_request_diff_id":27,"relative_order":4,"utf8_diff":"--- /dev/null\n+++ b/files/ruby/feature.rb\n@@ -0,0 +1,4 @@\n+# This file was changed in feature branch\n+# We put different code here to make merge conflict\n+class Conflict\n+end\n","new_path":"files/ruby/feature.rb","old_path":"files/ruby/feature.rb","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":27,"relative_order":5,"utf8_diff":"--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" => path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" => path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output << stdout.read\n @cmd_output << stderr.read\n","new_path":"files/ruby/popen.rb","old_path":"files/ruby/popen.rb","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":27,"relative_order":6,"utf8_diff":"--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n","new_path":"files/ruby/regex.rb","old_path":"files/ruby/regex.rb","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":27,"relative_order":7,"utf8_diff":"--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n","new_path":"gitlab-grack","old_path":"gitlab-grack","a_mode":"0","b_mode":"160000","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":27,"relative_order":8,"utf8_diff":"--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n","new_path":"gitlab-shell","old_path":"gitlab-shell","a_mode":"0","b_mode":"160000","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false}],"merge_request_id":27,"created_at":"2016-06-14T15:02:36.572Z","updated_at":"2016-06-14T15:02:36.658Z","base_commit_sha":"ae73cb07c9eeaf35924a10f713b364d32b2dd34f","real_size":"9"},"events":[{"id":221,"target_type":"MergeRequest","target_id":27,"project_id":36,"created_at":"2016-06-14T15:02:36.703Z","updated_at":"2016-06-14T15:02:36.703Z","action":1,"author_id":1},{"id":187,"target_type":"MergeRequest","target_id":27,"project_id":5,"created_at":"2016-06-14T15:02:36.703Z","updated_at":"2016-06-14T15:02:36.703Z","action":1,"author_id":1}],"approvals_before_merge":1,"award_emoji":[{"id":1,"name":"thumbsup","user_id":1,"awardable_type":"MergeRequest","awardable_id":27,"created_at":"2020-01-07T11:21:21.235Z","updated_at":"2020-01-07T11:21:21.235Z"},{"id":2,"name":"drum","user_id":1,"awardable_type":"MergeRequest","awardable_id":27,"created_at":"2020-01-07T11:21:21.235Z","updated_at":"2020-01-07T11:21:21.235Z"}]}
-{"id":26,"target_branch":"master","source_branch":"feature","source_project_id":4,"author_id":1,"assignee_id":null,"title":"MR2","created_at":"2016-06-14T15:02:36.418Z","updated_at":"2016-06-14T15:02:57.013Z","state":"opened","merge_status":"unchecked","target_project_id":5,"iid":8,"description":null,"position":0,"updated_by_id":null,"merge_error":null,"merge_params":{"force_remove_source_branch":null},"merge_when_pipeline_succeeds":false,"merge_user_id":null,"merge_commit_sha":null,"notes":[{"id":679,"note":"Qui rerum totam nisi est.","noteable_type":"MergeRequest","author_id":26,"created_at":"2016-06-14T15:02:56.848Z","updated_at":"2016-06-14T15:02:56.848Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":26,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":680,"note":"Pariatur magni corrupti consequatur debitis minima error beatae voluptatem.","noteable_type":"MergeRequest","author_id":25,"created_at":"2016-06-14T15:02:56.871Z","updated_at":"2016-06-14T15:02:56.871Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":26,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":681,"note":"Qui quis ut modi eos rerum ratione.","noteable_type":"MergeRequest","author_id":22,"created_at":"2016-06-14T15:02:56.895Z","updated_at":"2016-06-14T15:02:56.895Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":26,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":682,"note":"Illum quidem expedita mollitia fugit.","noteable_type":"MergeRequest","author_id":20,"created_at":"2016-06-14T15:02:56.918Z","updated_at":"2016-06-14T15:02:56.918Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":26,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":683,"note":"Consectetur voluptate sit sint possimus veritatis quod.","noteable_type":"MergeRequest","author_id":16,"created_at":"2016-06-14T15:02:56.942Z","updated_at":"2016-06-14T15:02:56.942Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":26,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":684,"note":"Natus libero quibusdam rem assumenda deleniti accusamus sed earum.","noteable_type":"MergeRequest","author_id":15,"created_at":"2016-06-14T15:02:56.966Z","updated_at":"2016-06-14T15:02:56.966Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":26,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":685,"note":"Tenetur autem nihil rerum odit.","noteable_type":"MergeRequest","author_id":6,"created_at":"2016-06-14T15:02:56.989Z","updated_at":"2016-06-14T15:02:56.989Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":26,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":686,"note":"Quia maiores et odio sed.","noteable_type":"MergeRequest","author_id":1,"created_at":"2016-06-14T15:02:57.012Z","updated_at":"2016-06-14T15:02:57.012Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":26,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"merge_request_diff":{"id":26,"state":"collected","merge_request_diff_commits":[{"merge_request_diff_id":26,"sha":"0b4bc9a49b562e85de7cc9e834518ea6828729b9","relative_order":0,"message":"Feature added\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:26:01.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T09:26:01.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}}],"merge_request_diff_files":[{"merge_request_diff_id":26,"relative_order":0,"utf8_diff":"--- /dev/null\n+++ b/files/ruby/feature.rb\n@@ -0,0 +1,5 @@\n+class Feature\n+ def foo\n+ puts 'bar'\n+ end\n+end\n","new_path":"files/ruby/feature.rb","old_path":"files/ruby/feature.rb","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false}],"merge_request_id":26,"created_at":"2016-06-14T15:02:36.421Z","updated_at":"2016-06-14T15:02:36.474Z","base_commit_sha":"ae73cb07c9eeaf35924a10f713b364d32b2dd34f","real_size":"1"},"events":[{"id":222,"target_type":"MergeRequest","target_id":26,"project_id":36,"created_at":"2016-06-14T15:02:36.496Z","updated_at":"2016-06-14T15:02:36.496Z","action":1,"author_id":1},{"id":186,"target_type":"MergeRequest","target_id":26,"project_id":5,"created_at":"2016-06-14T15:02:36.496Z","updated_at":"2016-06-14T15:02:36.496Z","action":1,"author_id":1}]}
-{"id":15,"target_branch":"test-7","source_branch":"test-1","source_project_id":5,"author_id":22,"assignee_id":16,"title":"Qui accusantium et inventore facilis doloribus occaecati officiis.","created_at":"2016-06-14T15:02:25.168Z","updated_at":"2016-06-14T15:02:59.521Z","state":"opened","merge_status":"unchecked","target_project_id":5,"iid":7,"description":"Et commodi deserunt aspernatur vero rerum. Ut non dolorum alias in odit est libero. Voluptatibus eos in et vitae repudiandae facilis ex mollitia.","position":0,"updated_by_id":null,"merge_error":null,"merge_params":{"force_remove_source_branch":null},"merge_when_pipeline_succeeds":false,"merge_user_id":null,"merge_commit_sha":null,"notes":[{"id":777,"note":"Pariatur voluptas placeat aspernatur culpa suscipit soluta.","noteable_type":"MergeRequest","author_id":26,"created_at":"2016-06-14T15:02:59.348Z","updated_at":"2016-06-14T15:02:59.348Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":15,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":778,"note":"Alias et iure mollitia suscipit molestiae voluptatum nostrum asperiores.","noteable_type":"MergeRequest","author_id":25,"created_at":"2016-06-14T15:02:59.372Z","updated_at":"2016-06-14T15:02:59.372Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":15,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":779,"note":"Laudantium qui eum qui sunt.","noteable_type":"MergeRequest","author_id":22,"created_at":"2016-06-14T15:02:59.395Z","updated_at":"2016-06-14T15:02:59.395Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":15,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":780,"note":"Quas rem est iusto ut delectus fugiat recusandae mollitia.","noteable_type":"MergeRequest","author_id":20,"created_at":"2016-06-14T15:02:59.418Z","updated_at":"2016-06-14T15:02:59.418Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":15,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":781,"note":"Repellendus ab et qui nesciunt.","noteable_type":"MergeRequest","author_id":16,"created_at":"2016-06-14T15:02:59.444Z","updated_at":"2016-06-14T15:02:59.444Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":15,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":782,"note":"Non possimus voluptatum odio qui ut.","noteable_type":"MergeRequest","author_id":15,"created_at":"2016-06-14T15:02:59.469Z","updated_at":"2016-06-14T15:02:59.469Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":15,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":783,"note":"Dolores repellendus eum ducimus quam ab dolorem quia.","noteable_type":"MergeRequest","author_id":6,"created_at":"2016-06-14T15:02:59.494Z","updated_at":"2016-06-14T15:02:59.494Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":15,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":784,"note":"Facilis dolorem aut corrupti id ratione occaecati.","noteable_type":"MergeRequest","author_id":1,"created_at":"2016-06-14T15:02:59.520Z","updated_at":"2016-06-14T15:02:59.520Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":15,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"merge_request_diff":{"id":15,"state":"collected","merge_request_diff_commits":[{"merge_request_diff_id":15,"relative_order":0,"sha":"94b8d581c48d894b86661718582fecbc5e3ed2eb","message":"fixes #10\n","authored_date":"2016-01-19T13:22:56.000+01:00","author_name":"James Lopez","author_email":"james@jameslopez.es","committed_date":"2016-01-19T13:22:56.000+01:00","committer_name":"James Lopez","committer_email":"james@jameslopez.es","commit_author":{"name":"James Lopez","email":"james@jameslopez.es"},"committer":{"name":"James Lopez","email":"james@jameslopez.es"}}],"merge_request_diff_files":[{"merge_request_diff_id":15,"relative_order":0,"utf8_diff":"--- /dev/null\n+++ b/test\n","new_path":"test","old_path":"test","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false}],"merge_request_id":15,"created_at":"2016-06-14T15:02:25.171Z","updated_at":"2016-06-14T15:02:25.230Z","base_commit_sha":"be93687618e4b132087f430a4d8fc3a609c9b77c","real_size":"1"},"events":[{"id":223,"target_type":"MergeRequest","target_id":15,"project_id":36,"created_at":"2016-06-14T15:02:25.262Z","updated_at":"2016-06-14T15:02:25.262Z","action":1,"author_id":1},{"id":175,"target_type":"MergeRequest","target_id":15,"project_id":5,"created_at":"2016-06-14T15:02:25.262Z","updated_at":"2016-06-14T15:02:25.262Z","action":1,"author_id":22}]}
-{"id":14,"target_branch":"fix","source_branch":"test-3","source_project_id":5,"author_id":20,"assignee_id":20,"title":"In voluptas aut sequi voluptatem ullam vel corporis illum consequatur.","created_at":"2016-06-14T15:02:24.760Z","updated_at":"2016-06-14T15:02:59.749Z","state":"opened","merge_status":"unchecked","target_project_id":5,"iid":6,"description":"Dicta magnam non voluptates nam dignissimos nostrum deserunt. Dolorum et suscipit iure quae doloremque. Necessitatibus saepe aut labore sed.","position":0,"updated_by_id":null,"merge_error":null,"merge_params":{"force_remove_source_branch":null},"merge_when_pipeline_succeeds":false,"merge_user_id":null,"merge_commit_sha":null,"notes":[{"id":785,"note":"Atque cupiditate necessitatibus deserunt minus natus odit.","noteable_type":"MergeRequest","author_id":26,"created_at":"2016-06-14T15:02:59.559Z","updated_at":"2016-06-14T15:02:59.559Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":14,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":786,"note":"Non dolorem provident mollitia nesciunt optio ex eveniet.","noteable_type":"MergeRequest","author_id":25,"created_at":"2016-06-14T15:02:59.587Z","updated_at":"2016-06-14T15:02:59.587Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":14,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":787,"note":"Similique officia nemo quasi commodi accusantium quae qui.","noteable_type":"MergeRequest","author_id":22,"created_at":"2016-06-14T15:02:59.621Z","updated_at":"2016-06-14T15:02:59.621Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":14,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":788,"note":"Et est et alias ad dolor qui.","noteable_type":"MergeRequest","author_id":20,"created_at":"2016-06-14T15:02:59.650Z","updated_at":"2016-06-14T15:02:59.650Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":14,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":789,"note":"Numquam temporibus ratione voluptatibus aliquid.","noteable_type":"MergeRequest","author_id":16,"created_at":"2016-06-14T15:02:59.675Z","updated_at":"2016-06-14T15:02:59.675Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":14,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":790,"note":"Ut ex aliquam consectetur perferendis est hic aut quia.","noteable_type":"MergeRequest","author_id":15,"created_at":"2016-06-14T15:02:59.703Z","updated_at":"2016-06-14T15:02:59.703Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":14,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":791,"note":"Esse eos quam quaerat aut ut asperiores officiis.","noteable_type":"MergeRequest","author_id":6,"created_at":"2016-06-14T15:02:59.726Z","updated_at":"2016-06-14T15:02:59.726Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":14,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":792,"note":"Sint facilis accusantium iure blanditiis.","noteable_type":"MergeRequest","author_id":1,"created_at":"2016-06-14T15:02:59.748Z","updated_at":"2016-06-14T15:02:59.748Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":14,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"merge_request_diff":{"id":14,"state":"collected","merge_request_diff_commits":[{"merge_request_diff_id":14,"relative_order":0,"sha":"ddd4ff416a931589c695eb4f5b23f844426f6928","message":"fixes #10\n","authored_date":"2016-01-19T14:14:43.000+01:00","author_name":"James Lopez","author_email":"james@jameslopez.es","committed_date":"2016-01-19T14:14:43.000+01:00","committer_name":"James Lopez","committer_email":"james@jameslopez.es","commit_author":{"name":"James Lopez","email":"james@jameslopez.es"},"committer":{"name":"James Lopez","email":"james@jameslopez.es"}},{"merge_request_diff_id":14,"relative_order":1,"sha":"be93687618e4b132087f430a4d8fc3a609c9b77c","message":"Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6","authored_date":"2015-12-07T12:52:12.000+01:00","author_name":"Marin Jankovski","author_email":"marin@gitlab.com","committed_date":"2015-12-07T12:52:12.000+01:00","committer_name":"Marin Jankovski","committer_email":"marin@gitlab.com","commit_author":{"name":"Marin Jankovski","email":"marin@gitlab.com"},"committer":{"name":"Marin Jankovski","email":"marin@gitlab.com"}},{"merge_request_diff_id":14,"relative_order":2,"sha":"048721d90c449b244b7b4c53a9186b04330174ec","message":"LFS object pointer.\n","authored_date":"2015-12-07T11:54:28.000+01:00","author_name":"Marin Jankovski","author_email":"maxlazio@gmail.com","committed_date":"2015-12-07T11:54:28.000+01:00","committer_name":"Marin Jankovski","committer_email":"maxlazio@gmail.com","commit_author":{"name":"Marin Jankovski","email":"maxlazio@gmail.com"},"committer":{"name":"Marin Jankovski","email":"maxlazio@gmail.com"}},{"merge_request_diff_id":14,"relative_order":3,"sha":"5f923865dde3436854e9ceb9cdb7815618d4e849","message":"GitLab currently doesn't support patches that involve a merge commit: add a commit here\n","authored_date":"2015-11-13T16:27:12.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T16:27:12.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":14,"relative_order":4,"sha":"d2d430676773caa88cdaf7c55944073b2fd5561a","message":"Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5","authored_date":"2015-11-13T08:50:17.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T08:50:17.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":14,"relative_order":5,"sha":"2ea1f3dec713d940208fb5ce4a38765ecb5d3f73","message":"Add GitLab SVG\n","authored_date":"2015-11-13T08:39:43.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T08:39:43.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":14,"relative_order":6,"sha":"59e29889be61e6e0e5e223bfa9ac2721d31605b8","message":"Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4","authored_date":"2015-11-13T07:21:40.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T07:21:40.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":14,"relative_order":7,"sha":"66eceea0db202bb39c4e445e8ca28689645366c5","message":"add spaces in whitespace file\n","authored_date":"2015-11-13T06:01:27.000+01:00","author_name":"윤민식","author_email":"minsik.yoon@samsung.com","committed_date":"2015-11-13T06:01:27.000+01:00","committer_name":"윤민식","committer_email":"minsik.yoon@samsung.com","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":14,"relative_order":8,"sha":"08f22f255f082689c0d7d39d19205085311542bc","message":"remove empty file.(beacase git ignore empty file)\nadd whitespace test file.\n","authored_date":"2015-11-13T06:00:16.000+01:00","author_name":"윤민식","author_email":"minsik.yoon@samsung.com","committed_date":"2015-11-13T06:00:16.000+01:00","committer_name":"윤민식","committer_email":"minsik.yoon@samsung.com","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":14,"relative_order":9,"sha":"19e2e9b4ef76b422ce1154af39a91323ccc57434","message":"Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3","authored_date":"2015-11-13T05:23:14.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T05:23:14.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":14,"relative_order":10,"sha":"c642fe9b8b9f28f9225d7ea953fe14e74748d53b","message":"add whitespace in empty\n","authored_date":"2015-11-13T05:08:45.000+01:00","author_name":"윤민식","author_email":"minsik.yoon@samsung.com","committed_date":"2015-11-13T05:08:45.000+01:00","committer_name":"윤민식","committer_email":"minsik.yoon@samsung.com","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":14,"relative_order":11,"sha":"9a944d90955aaf45f6d0c88f30e27f8d2c41cec0","message":"add empty file\n","authored_date":"2015-11-13T05:08:04.000+01:00","author_name":"윤민식","author_email":"minsik.yoon@samsung.com","committed_date":"2015-11-13T05:08:04.000+01:00","committer_name":"윤민식","committer_email":"minsik.yoon@samsung.com","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":14,"relative_order":12,"sha":"c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd","message":"Add ISO-8859 test file\n","authored_date":"2015-08-25T17:53:12.000+02:00","author_name":"Stan Hu","author_email":"stanhu@packetzoom.com","committed_date":"2015-08-25T17:53:12.000+02:00","committer_name":"Stan Hu","committer_email":"stanhu@packetzoom.com","commit_author":{"name":"Stan Hu","email":"stanhu@packetzoom.com"},"committer":{"name":"Stan Hu","email":"stanhu@packetzoom.com"}},{"merge_request_diff_id":14,"relative_order":13,"sha":"e56497bb5f03a90a51293fc6d516788730953899","message":"Merge branch 'tree_helper_spec' into 'master'\n\nAdd directory structure for tree_helper spec\n\nThis directory structure is needed for a testing the method flatten_tree(tree) in the TreeHelper module\n\nSee [merge request #275](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/275#note_732774)\n\nSee merge request !2\n","authored_date":"2015-01-10T22:23:29.000+01:00","author_name":"Sytse Sijbrandij","author_email":"sytse@gitlab.com","committed_date":"2015-01-10T22:23:29.000+01:00","committer_name":"Sytse Sijbrandij","committer_email":"sytse@gitlab.com","commit_author":{"name":"Sytse Sijbrandij","email":"sytse@gitlab.com"},"committer":{"name":"Sytse Sijbrandij","email":"sytse@gitlab.com"}},{"merge_request_diff_id":14,"relative_order":14,"sha":"4cd80ccab63c82b4bad16faa5193fbd2aa06df40","message":"add directory structure for tree_helper spec\n","authored_date":"2015-01-10T21:28:18.000+01:00","author_name":"marmis85","author_email":"marmis85@gmail.com","committed_date":"2015-01-10T21:28:18.000+01:00","committer_name":"marmis85","committer_email":"marmis85@gmail.com","commit_author":{"name":"marmis85","email":"marmis85@gmail.com"},"committer":{"name":"marmis85","email":"marmis85@gmail.com"}},{"merge_request_diff_id":14,"relative_order":15,"sha":"5937ac0a7beb003549fc5fd26fc247adbce4a52e","message":"Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T10:01:38.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T10:01:38.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":14,"relative_order":16,"sha":"570e7b2abdd848b95f2f578043fc23bd6f6fd24d","message":"Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:57:31.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T09:57:31.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":14,"relative_order":17,"sha":"6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9","message":"More submodules\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:54:21.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T09:54:21.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":14,"relative_order":18,"sha":"d14d6c0abdd253381df51a723d58691b2ee1ab08","message":"Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:49:50.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T09:49:50.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":14,"relative_order":19,"sha":"c1acaa58bbcbc3eafe538cb8274ba387047b69f8","message":"Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:48:32.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T09:48:32.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}}],"merge_request_diff_files":[{"merge_request_diff_id":14,"relative_order":0,"utf8_diff":"Binary files a/.DS_Store and /dev/null differ\n","new_path":".DS_Store","old_path":".DS_Store","a_mode":"100644","b_mode":"0","new_file":false,"renamed_file":false,"deleted_file":true,"too_large":false},{"merge_request_diff_id":14,"relative_order":1,"utf8_diff":"--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n","new_path":".gitignore","old_path":".gitignore","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":2,"utf8_diff":"--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n","new_path":".gitmodules","old_path":".gitmodules","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":3,"utf8_diff":"--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n","new_path":"CHANGELOG","old_path":"CHANGELOG","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":4,"utf8_diff":"--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n","new_path":"encoding/iso8859.txt","old_path":"encoding/iso8859.txt","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":5,"utf8_diff":"Binary files a/files/.DS_Store and /dev/null differ\n","new_path":"files/.DS_Store","old_path":"files/.DS_Store","a_mode":"100644","b_mode":"0","new_file":false,"renamed_file":false,"deleted_file":true,"too_large":false},{"merge_request_diff_id":14,"relative_order":6,"utf8_diff":"--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n+<svg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\">\n+ <!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->\n+ <title>wm</title>\n+ <desc>Created with Sketch.</desc>\n+ <defs>\n+ <path id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"></path>\n+ </defs>\n+ <g id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\">\n+ <path d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\">\n+ <g id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\">\n+ <g id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\">\n+ <path d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"></path>\n+ </g>\n+ <g id=\"g16\">\n+ <g id=\"g18-Clipped\">\n+ <mask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\">\n+ <use xlink:href=\"#path-1\"></use>\n+ </mask>\n+ <g id=\"path22\"></g>\n+ <g id=\"g18\" mask=\"url(#mask-2)\">\n+ <g transform=\"translate(382.736659, 312.879425)\">\n+ <g id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\">\n+ <path d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\">\n+ <path d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\">\n+ <path d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\">\n+ <path d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <path d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <path d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\">\n+ <path d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\">\n+ <path d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path54\"></g>\n+ </g>\n+ <g id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\">\n+ <path d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <g id=\"path62\"></g>\n+ </g>\n+ <g id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\">\n+ <path d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path70\"></g>\n+ </g>\n+ <g id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\">\n+ <path d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <path d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\">\n+ <path d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\">\n+ <path d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+</svg>\n\\ No newline at end of file\n","new_path":"files/images/wm.svg","old_path":"files/images/wm.svg","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":7,"utf8_diff":"--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n","new_path":"files/lfs/lfs_object.iso","old_path":"files/lfs/lfs_object.iso","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":8,"utf8_diff":"--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" => path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" => path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output << stdout.read\n @cmd_output << stderr.read\n","new_path":"files/ruby/popen.rb","old_path":"files/ruby/popen.rb","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":9,"utf8_diff":"--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n","new_path":"files/ruby/regex.rb","old_path":"files/ruby/regex.rb","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":10,"utf8_diff":"--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n","new_path":"files/whitespace","old_path":"files/whitespace","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":11,"utf8_diff":"--- /dev/null\n+++ b/foo/bar/.gitkeep\n","new_path":"foo/bar/.gitkeep","old_path":"foo/bar/.gitkeep","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":12,"utf8_diff":"--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n","new_path":"gitlab-grack","old_path":"gitlab-grack","a_mode":"0","b_mode":"160000","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":13,"utf8_diff":"--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n","new_path":"gitlab-shell","old_path":"gitlab-shell","a_mode":"0","b_mode":"160000","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":14,"utf8_diff":"--- /dev/null\n+++ b/test\n","new_path":"test","old_path":"test","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false}],"merge_request_id":14,"created_at":"2016-06-14T15:02:24.770Z","updated_at":"2016-06-14T15:02:25.007Z","base_commit_sha":"ae73cb07c9eeaf35924a10f713b364d32b2dd34f","real_size":"15"},"events":[{"id":224,"target_type":"MergeRequest","target_id":14,"project_id":36,"created_at":"2016-06-14T15:02:25.113Z","updated_at":"2016-06-14T15:02:25.113Z","action":1,"author_id":1},{"id":174,"target_type":"MergeRequest","target_id":14,"project_id":5,"created_at":"2016-06-14T15:02:25.113Z","updated_at":"2016-06-14T15:02:25.113Z","action":1,"author_id":20}]}
-{"id":13,"target_branch":"improve/awesome","source_branch":"test-8","source_project_id":5,"author_id":16,"assignee_id":25,"title":"Voluptates consequatur eius nemo amet libero animi illum delectus tempore.","created_at":"2016-06-14T15:02:24.415Z","updated_at":"2016-06-14T15:02:59.958Z","state":"opened","merge_status":"unchecked","target_project_id":5,"iid":5,"description":"Est eaque quasi qui qui. Similique voluptatem impedit iusto ratione reprehenderit. Itaque est illum ut nulla aut.","position":0,"updated_by_id":null,"merge_error":null,"merge_params":{"force_remove_source_branch":null},"merge_when_pipeline_succeeds":false,"merge_user_id":null,"merge_commit_sha":null,"notes":[{"id":793,"note":"In illum maxime aperiam nulla est aspernatur.","noteable_type":"MergeRequest","author_id":26,"created_at":"2016-06-14T15:02:59.782Z","updated_at":"2016-06-14T15:02:59.782Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[{"merge_request_diff_id":14,"id":529,"target_type":"Note","target_id":793,"project_id":4,"created_at":"2016-07-07T14:35:12.128Z","updated_at":"2016-07-07T14:35:12.128Z","action":6,"author_id":1}]},{"id":794,"note":"Enim quia perferendis cum distinctio tenetur optio voluptas veniam.","noteable_type":"MergeRequest","author_id":25,"created_at":"2016-06-14T15:02:59.807Z","updated_at":"2016-06-14T15:02:59.807Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":795,"note":"Dolor ad quia quis pariatur ducimus.","noteable_type":"MergeRequest","author_id":22,"created_at":"2016-06-14T15:02:59.831Z","updated_at":"2016-06-14T15:02:59.831Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":796,"note":"Et a odio voluptate aut.","noteable_type":"MergeRequest","author_id":20,"created_at":"2016-06-14T15:02:59.854Z","updated_at":"2016-06-14T15:02:59.854Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":797,"note":"Quis nihil temporibus voluptatum modi minima a ut.","noteable_type":"MergeRequest","author_id":16,"created_at":"2016-06-14T15:02:59.879Z","updated_at":"2016-06-14T15:02:59.879Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":798,"note":"Ut alias consequatur in nostrum.","noteable_type":"MergeRequest","author_id":15,"created_at":"2016-06-14T15:02:59.904Z","updated_at":"2016-06-14T15:02:59.904Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":799,"note":"Voluptatibus aperiam assumenda et neque sint libero.","noteable_type":"MergeRequest","author_id":6,"created_at":"2016-06-14T15:02:59.926Z","updated_at":"2016-06-14T15:02:59.926Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":800,"note":"Veritatis voluptatem dolor dolores magni quo ut ipsa fuga.","noteable_type":"MergeRequest","author_id":1,"created_at":"2016-06-14T15:02:59.956Z","updated_at":"2016-06-14T15:02:59.956Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"merge_request_diff":{"id":13,"state":"collected","merge_request_diff_commits":[{"merge_request_diff_id":13,"relative_order":0,"sha":"0bfedc29d30280c7e8564e19f654584b459e5868","message":"fixes #10\n","authored_date":"2016-01-19T15:25:23.000+01:00","author_name":"James Lopez","author_email":"james@jameslopez.es","committed_date":"2016-01-19T15:25:23.000+01:00","committer_name":"James Lopez","committer_email":"james@jameslopez.es","commit_author":{"name":"James Lopez","email":"james@jameslopez.es"},"committer":{"name":"James Lopez","email":"james@jameslopez.es"}},{"merge_request_diff_id":13,"relative_order":1,"sha":"be93687618e4b132087f430a4d8fc3a609c9b77c","message":"Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6","authored_date":"2015-12-07T12:52:12.000+01:00","author_name":"Marin Jankovski","author_email":"marin@gitlab.com","committed_date":"2015-12-07T12:52:12.000+01:00","committer_name":"Marin Jankovski","committer_email":"marin@gitlab.com","commit_author":{"name":"Marin Jankovski","email":"marin@gitlab.com"},"committer":{"name":"Marin Jankovski","email":"marin@gitlab.com"}},{"merge_request_diff_id":13,"relative_order":2,"sha":"048721d90c449b244b7b4c53a9186b04330174ec","message":"LFS object pointer.\n","authored_date":"2015-12-07T11:54:28.000+01:00","author_name":"Marin Jankovski","author_email":"maxlazio@gmail.com","committed_date":"2015-12-07T11:54:28.000+01:00","committer_name":"Marin Jankovski","committer_email":"maxlazio@gmail.com","commit_author":{"name":"Marin Jankovski","email":"maxlazio@gmail.com"},"committer":{"name":"Marin Jankovski","email":"maxlazio@gmail.com"}},{"merge_request_diff_id":13,"relative_order":3,"sha":"5f923865dde3436854e9ceb9cdb7815618d4e849","message":"GitLab currently doesn't support patches that involve a merge commit: add a commit here\n","authored_date":"2015-11-13T16:27:12.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T16:27:12.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":13,"relative_order":4,"sha":"d2d430676773caa88cdaf7c55944073b2fd5561a","message":"Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5","authored_date":"2015-11-13T08:50:17.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T08:50:17.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":13,"relative_order":5,"sha":"2ea1f3dec713d940208fb5ce4a38765ecb5d3f73","message":"Add GitLab SVG\n","authored_date":"2015-11-13T08:39:43.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T08:39:43.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":13,"relative_order":6,"sha":"59e29889be61e6e0e5e223bfa9ac2721d31605b8","message":"Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4","authored_date":"2015-11-13T07:21:40.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T07:21:40.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":13,"relative_order":7,"sha":"66eceea0db202bb39c4e445e8ca28689645366c5","message":"add spaces in whitespace file\n","authored_date":"2015-11-13T06:01:27.000+01:00","author_name":"윤민식","author_email":"minsik.yoon@samsung.com","committed_date":"2015-11-13T06:01:27.000+01:00","committer_name":"윤민식","committer_email":"minsik.yoon@samsung.com","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":13,"relative_order":8,"sha":"08f22f255f082689c0d7d39d19205085311542bc","message":"remove empty file.(beacase git ignore empty file)\nadd whitespace test file.\n","authored_date":"2015-11-13T06:00:16.000+01:00","author_name":"윤민식","author_email":"minsik.yoon@samsung.com","committed_date":"2015-11-13T06:00:16.000+01:00","committer_name":"윤민식","committer_email":"minsik.yoon@samsung.com","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":13,"relative_order":9,"sha":"19e2e9b4ef76b422ce1154af39a91323ccc57434","message":"Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3","authored_date":"2015-11-13T05:23:14.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T05:23:14.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":13,"relative_order":10,"sha":"c642fe9b8b9f28f9225d7ea953fe14e74748d53b","message":"add whitespace in empty\n","authored_date":"2015-11-13T05:08:45.000+01:00","author_name":"윤민식","author_email":"minsik.yoon@samsung.com","committed_date":"2015-11-13T05:08:45.000+01:00","committer_name":"윤민식","committer_email":"minsik.yoon@samsung.com","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":13,"relative_order":11,"sha":"9a944d90955aaf45f6d0c88f30e27f8d2c41cec0","message":"add empty file\n","authored_date":"2015-11-13T05:08:04.000+01:00","author_name":"윤민식","author_email":"minsik.yoon@samsung.com","committed_date":"2015-11-13T05:08:04.000+01:00","committer_name":"윤민식","committer_email":"minsik.yoon@samsung.com","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":13,"relative_order":12,"sha":"c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd","message":"Add ISO-8859 test file\n","authored_date":"2015-08-25T17:53:12.000+02:00","author_name":"Stan Hu","author_email":"stanhu@packetzoom.com","committed_date":"2015-08-25T17:53:12.000+02:00","committer_name":"Stan Hu","committer_email":"stanhu@packetzoom.com","commit_author":{"name":"Stan Hu","email":"stanhu@packetzoom.com"},"committer":{"name":"Stan Hu","email":"stanhu@packetzoom.com"}},{"merge_request_diff_id":13,"relative_order":13,"sha":"e56497bb5f03a90a51293fc6d516788730953899","message":"Merge branch 'tree_helper_spec' into 'master'\n\nAdd directory structure for tree_helper spec\n\nThis directory structure is needed for a testing the method flatten_tree(tree) in the TreeHelper module\n\nSee [merge request #275](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/275#note_732774)\n\nSee merge request !2\n","authored_date":"2015-01-10T22:23:29.000+01:00","author_name":"Sytse Sijbrandij","author_email":"sytse@gitlab.com","committed_date":"2015-01-10T22:23:29.000+01:00","committer_name":"Sytse Sijbrandij","committer_email":"sytse@gitlab.com","commit_author":{"name":"Sytse Sijbrandij","email":"sytse@gitlab.com"},"committer":{"name":"Sytse Sijbrandij","email":"sytse@gitlab.com"}},{"merge_request_diff_id":13,"relative_order":14,"sha":"4cd80ccab63c82b4bad16faa5193fbd2aa06df40","message":"add directory structure for tree_helper spec\n","authored_date":"2015-01-10T21:28:18.000+01:00","author_name":"marmis85","author_email":"marmis85@gmail.com","committed_date":"2015-01-10T21:28:18.000+01:00","committer_name":"marmis85","committer_email":"marmis85@gmail.com","commit_author":{"name":"marmis85","email":"marmis85@gmail.com"},"committer":{"name":"marmis85","email":"marmis85@gmail.com"}}],"merge_request_diff_files":[{"merge_request_diff_id":13,"relative_order":0,"utf8_diff":"--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n","new_path":"CHANGELOG","old_path":"CHANGELOG","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":13,"relative_order":1,"utf8_diff":"--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n","new_path":"encoding/iso8859.txt","old_path":"encoding/iso8859.txt","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":13,"relative_order":2,"utf8_diff":"--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n+<svg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\">\n+ <!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->\n+ <title>wm</title>\n+ <desc>Created with Sketch.</desc>\n+ <defs>\n+ <path id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"></path>\n+ </defs>\n+ <g id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\">\n+ <path d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\">\n+ <g id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\">\n+ <g id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\">\n+ <path d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"></path>\n+ </g>\n+ <g id=\"g16\">\n+ <g id=\"g18-Clipped\">\n+ <mask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\">\n+ <use xlink:href=\"#path-1\"></use>\n+ </mask>\n+ <g id=\"path22\"></g>\n+ <g id=\"g18\" mask=\"url(#mask-2)\">\n+ <g transform=\"translate(382.736659, 312.879425)\">\n+ <g id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\">\n+ <path d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\">\n+ <path d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\">\n+ <path d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\">\n+ <path d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <path d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <path d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\">\n+ <path d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\">\n+ <path d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path54\"></g>\n+ </g>\n+ <g id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\">\n+ <path d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <g id=\"path62\"></g>\n+ </g>\n+ <g id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\">\n+ <path d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path70\"></g>\n+ </g>\n+ <g id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\">\n+ <path d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <path d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\">\n+ <path d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\">\n+ <path d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+</svg>\n\\ No newline at end of file\n","new_path":"files/images/wm.svg","old_path":"files/images/wm.svg","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":13,"relative_order":3,"utf8_diff":"--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n","new_path":"files/lfs/lfs_object.iso","old_path":"files/lfs/lfs_object.iso","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":13,"relative_order":4,"utf8_diff":"--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n","new_path":"files/whitespace","old_path":"files/whitespace","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":13,"relative_order":5,"utf8_diff":"--- /dev/null\n+++ b/foo/bar/.gitkeep\n","new_path":"foo/bar/.gitkeep","old_path":"foo/bar/.gitkeep","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":13,"relative_order":6,"utf8_diff":"--- /dev/null\n+++ b/test\n","new_path":"test","old_path":"test","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false}],"merge_request_id":13,"created_at":"2016-06-14T15:02:24.420Z","updated_at":"2016-06-14T15:02:24.561Z","base_commit_sha":"5937ac0a7beb003549fc5fd26fc247adbce4a52e","real_size":"7"},"events":[{"id":225,"target_type":"MergeRequest","target_id":13,"project_id":36,"created_at":"2016-06-14T15:02:24.636Z","updated_at":"2016-06-14T15:02:24.636Z","action":1,"author_id":16},{"id":173,"target_type":"MergeRequest","target_id":13,"project_id":5,"created_at":"2016-06-14T15:02:24.636Z","updated_at":"2016-06-14T15:02:24.636Z","action":1,"author_id":16}]}
-{"id":12,"target_branch":"flatten-dirs","source_branch":"test-2","source_project_id":5,"author_id":1,"assignee_id":22,"title":"In a rerum harum nihil accusamus aut quia nobis non.","created_at":"2016-06-14T15:02:24.000Z","updated_at":"2016-06-14T15:03:00.225Z","state":"opened","merge_status":"unchecked","target_project_id":5,"iid":4,"description":"Nam magnam odit velit rerum. Sapiente dolore sunt saepe debitis. Culpa maiores ut ad dolores dolorem et.","position":0,"updated_by_id":null,"merge_error":null,"merge_params":{"force_remove_source_branch":null},"merge_when_pipeline_succeeds":false,"merge_user_id":null,"merge_commit_sha":null,"notes":[{"id":801,"note":"Nihil dicta molestias expedita atque.","noteable_type":"MergeRequest","author_id":26,"created_at":"2016-06-14T15:03:00.001Z","updated_at":"2016-06-14T15:03:00.001Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":12,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":802,"note":"Illum culpa voluptas enim accusantium deserunt.","noteable_type":"MergeRequest","author_id":25,"created_at":"2016-06-14T15:03:00.034Z","updated_at":"2016-06-14T15:03:00.034Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":12,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":803,"note":"Dicta esse aliquam laboriosam unde alias.","noteable_type":"MergeRequest","author_id":22,"created_at":"2016-06-14T15:03:00.065Z","updated_at":"2016-06-14T15:03:00.065Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":12,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":804,"note":"Dicta autem et sed molestiae ut quae.","noteable_type":"MergeRequest","author_id":20,"created_at":"2016-06-14T15:03:00.097Z","updated_at":"2016-06-14T15:03:00.097Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":12,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":805,"note":"Ut ut temporibus voluptas dolore quia velit.","noteable_type":"MergeRequest","author_id":16,"created_at":"2016-06-14T15:03:00.129Z","updated_at":"2016-06-14T15:03:00.129Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":12,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":806,"note":"Dolores similique sint pariatur error id quia fugit aut.","noteable_type":"MergeRequest","author_id":15,"created_at":"2016-06-14T15:03:00.162Z","updated_at":"2016-06-14T15:03:00.162Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":12,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":807,"note":"Quisquam provident nihil aperiam voluptatem.","noteable_type":"MergeRequest","author_id":6,"created_at":"2016-06-14T15:03:00.193Z","updated_at":"2016-06-14T15:03:00.193Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":12,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":808,"note":"Similique quo vero expedita deserunt ipsam earum.","noteable_type":"MergeRequest","author_id":1,"created_at":"2016-06-14T15:03:00.224Z","updated_at":"2016-06-14T15:03:00.224Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":12,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"merge_request_diff":{"id":12,"state":"collected","merge_request_diff_commits":[{"merge_request_diff_id":12,"relative_order":0,"sha":"97a0df9696e2aebf10c31b3016f40214e0e8f243","message":"fixes #10\n","authored_date":"2016-01-19T14:08:21.000+01:00","author_name":"James Lopez","author_email":"james@jameslopez.es","committed_date":"2016-01-19T14:08:21.000+01:00","committer_name":"James Lopez","committer_email":"james@jameslopez.es","commit_author":{"name":"James Lopez","email":"james@jameslopez.es"},"committer":{"name":"James Lopez","email":"james@jameslopez.es"}},{"merge_request_diff_id":12,"relative_order":1,"sha":"be93687618e4b132087f430a4d8fc3a609c9b77c","message":"Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6","authored_date":"2015-12-07T12:52:12.000+01:00","author_name":"Marin Jankovski","author_email":"marin@gitlab.com","committed_date":"2015-12-07T12:52:12.000+01:00","committer_name":"Marin Jankovski","committer_email":"marin@gitlab.com","commit_author":{"name":"Marin Jankovski","email":"marin@gitlab.com"},"committer":{"name":"Marin Jankovski","email":"marin@gitlab.com"}},{"merge_request_diff_id":12,"relative_order":2,"sha":"048721d90c449b244b7b4c53a9186b04330174ec","message":"LFS object pointer.\n","authored_date":"2015-12-07T11:54:28.000+01:00","author_name":"Marin Jankovski","author_email":"maxlazio@gmail.com","committed_date":"2015-12-07T11:54:28.000+01:00","committer_name":"Marin Jankovski","committer_email":"maxlazio@gmail.com","commit_author":{"name":"Marin Jankovski","email":"maxlazio@gmail.com"},"committer":{"name":"Marin Jankovski","email":"maxlazio@gmail.com"}},{"merge_request_diff_id":12,"relative_order":3,"sha":"5f923865dde3436854e9ceb9cdb7815618d4e849","message":"GitLab currently doesn't support patches that involve a merge commit: add a commit here\n","authored_date":"2015-11-13T16:27:12.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T16:27:12.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":12,"relative_order":4,"sha":"d2d430676773caa88cdaf7c55944073b2fd5561a","message":"Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5","authored_date":"2015-11-13T08:50:17.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T08:50:17.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":12,"relative_order":5,"sha":"2ea1f3dec713d940208fb5ce4a38765ecb5d3f73","message":"Add GitLab SVG\n","authored_date":"2015-11-13T08:39:43.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T08:39:43.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":12,"relative_order":6,"sha":"59e29889be61e6e0e5e223bfa9ac2721d31605b8","message":"Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4","authored_date":"2015-11-13T07:21:40.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T07:21:40.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":12,"relative_order":7,"sha":"66eceea0db202bb39c4e445e8ca28689645366c5","message":"add spaces in whitespace file\n","authored_date":"2015-11-13T06:01:27.000+01:00","author_name":"윤민식","author_email":"minsik.yoon@samsung.com","committed_date":"2015-11-13T06:01:27.000+01:00","committer_name":"윤민식","committer_email":"minsik.yoon@samsung.com","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":12,"relative_order":8,"sha":"08f22f255f082689c0d7d39d19205085311542bc","message":"remove empty file.(beacase git ignore empty file)\nadd whitespace test file.\n","authored_date":"2015-11-13T06:00:16.000+01:00","author_name":"윤민식","author_email":"minsik.yoon@samsung.com","committed_date":"2015-11-13T06:00:16.000+01:00","committer_name":"윤민식","committer_email":"minsik.yoon@samsung.com","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":12,"relative_order":9,"sha":"19e2e9b4ef76b422ce1154af39a91323ccc57434","message":"Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3","authored_date":"2015-11-13T05:23:14.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T05:23:14.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":12,"relative_order":10,"sha":"c642fe9b8b9f28f9225d7ea953fe14e74748d53b","message":"add whitespace in empty\n","authored_date":"2015-11-13T05:08:45.000+01:00","author_name":"윤민식","author_email":"minsik.yoon@samsung.com","committed_date":"2015-11-13T05:08:45.000+01:00","committer_name":"윤민식","committer_email":"minsik.yoon@samsung.com","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":12,"relative_order":11,"sha":"9a944d90955aaf45f6d0c88f30e27f8d2c41cec0","message":"add empty file\n","authored_date":"2015-11-13T05:08:04.000+01:00","author_name":"윤민식","author_email":"minsik.yoon@samsung.com","committed_date":"2015-11-13T05:08:04.000+01:00","committer_name":"윤민식","committer_email":"minsik.yoon@samsung.com","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":12,"relative_order":12,"sha":"c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd","message":"Add ISO-8859 test file\n","authored_date":"2015-08-25T17:53:12.000+02:00","author_name":"Stan Hu","author_email":"stanhu@packetzoom.com","committed_date":"2015-08-25T17:53:12.000+02:00","committer_name":"Stan Hu","committer_email":"stanhu@packetzoom.com","commit_author":{"name":"Stan Hu","email":"stanhu@packetzoom.com"},"committer":{"name":"Stan Hu","email":"stanhu@packetzoom.com"}}],"merge_request_diff_files":[{"merge_request_diff_id":12,"relative_order":0,"utf8_diff":"--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n","new_path":"CHANGELOG","old_path":"CHANGELOG","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":12,"relative_order":1,"utf8_diff":"--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n","new_path":"encoding/iso8859.txt","old_path":"encoding/iso8859.txt","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":12,"relative_order":2,"utf8_diff":"--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n+<svg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\">\n+ <!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->\n+ <title>wm</title>\n+ <desc>Created with Sketch.</desc>\n+ <defs>\n+ <path id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"></path>\n+ </defs>\n+ <g id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\">\n+ <path d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\">\n+ <g id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\">\n+ <g id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\">\n+ <path d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"></path>\n+ </g>\n+ <g id=\"g16\">\n+ <g id=\"g18-Clipped\">\n+ <mask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\">\n+ <use xlink:href=\"#path-1\"></use>\n+ </mask>\n+ <g id=\"path22\"></g>\n+ <g id=\"g18\" mask=\"url(#mask-2)\">\n+ <g transform=\"translate(382.736659, 312.879425)\">\n+ <g id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\">\n+ <path d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\">\n+ <path d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\">\n+ <path d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\">\n+ <path d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <path d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <path d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\">\n+ <path d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\">\n+ <path d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path54\"></g>\n+ </g>\n+ <g id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\">\n+ <path d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <g id=\"path62\"></g>\n+ </g>\n+ <g id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\">\n+ <path d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path70\"></g>\n+ </g>\n+ <g id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\">\n+ <path d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <path d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\">\n+ <path d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\">\n+ <path d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+</svg>\n\\ No newline at end of file\n","new_path":"files/images/wm.svg","old_path":"files/images/wm.svg","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":12,"relative_order":3,"utf8_diff":"--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n","new_path":"files/lfs/lfs_object.iso","old_path":"files/lfs/lfs_object.iso","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":12,"relative_order":4,"utf8_diff":"--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n","new_path":"files/whitespace","old_path":"files/whitespace","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":12,"relative_order":5,"utf8_diff":"--- /dev/null\n+++ b/test\n","new_path":"test","old_path":"test","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false}],"merge_request_id":12,"created_at":"2016-06-14T15:02:24.006Z","updated_at":"2016-06-14T15:02:24.169Z","base_commit_sha":"e56497bb5f03a90a51293fc6d516788730953899","real_size":"6"},"events":[{"id":226,"target_type":"MergeRequest","target_id":12,"project_id":36,"created_at":"2016-06-14T15:02:24.253Z","updated_at":"2016-06-14T15:02:24.253Z","action":1,"author_id":1},{"id":172,"target_type":"MergeRequest","target_id":12,"project_id":5,"created_at":"2016-06-14T15:02:24.253Z","updated_at":"2016-06-14T15:02:24.253Z","action":1,"author_id":1}]}
+{"id":27,"target_branch":"feature","source_branch":"feature_conflict","source_project_id":2147483547,"author_id":1,"assignee_id":null,"title":"MR1","created_at":"2016-06-14T15:02:36.568Z","updated_at":"2016-06-14T15:02:56.815Z","state":"opened","merge_status":"unchecked","target_project_id":5,"iid":9,"description":null,"position":0,"updated_by_id":null,"merge_error":null,"diff_head_sha":"HEAD","source_branch_sha":"ABCD","target_branch_sha":"DCBA","merge_params":{"force_remove_source_branch":null},"merge_when_pipeline_succeeds":true,"merge_user_id":null,"merge_commit_sha":null,"notes":[{"id":669,"note":"added 3 commits\n\n<ul><li>16ea4e20...074a2a32 - 2 commits from branch <code>master</code></li><li>ca223a02 - readme: fix typos</li></ul>\n\n[Compare with previous version](/group/project/merge_requests/1/diffs?diff_id=1189&start_sha=16ea4e207fb258fe4e9c73185a725207c9a4f3e1)","noteable_type":"MergeRequest","author_id":26,"created_at":"2020-03-28T12:47:33.461Z","updated_at":"2020-03-28T12:47:33.461Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"system":true,"st_diff":null,"updated_by_id":null,"position":null,"original_position":null,"resolved_at":null,"resolved_by_id":null,"discussion_id":null,"change_position":null,"resolved_by_push":null,"confidential":null,"type":null,"author":{"name":"User 4"},"award_emoji":[],"system_note_metadata":{"id":4789,"commit_count":3,"action":"commit","created_at":"2020-03-28T12:47:33.461Z","updated_at":"2020-03-28T12:47:33.461Z"},"events":[],"suggestions":[]},{"id":670,"note":"unmarked as a **Work In Progress**","noteable_type":"MergeRequest","author_id":26,"created_at":"2020-03-28T12:48:36.951Z","updated_at":"2020-03-28T12:48:36.951Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"system":true,"st_diff":null,"updated_by_id":null,"position":null,"original_position":null,"resolved_at":null,"resolved_by_id":null,"discussion_id":null,"change_position":null,"resolved_by_push":null,"confidential":null,"type":null,"author":{"name":"User 4"},"award_emoji":[],"system_note_metadata":{"id":4790,"commit_count":null,"action":"title","created_at":"2020-03-28T12:48:36.951Z","updated_at":"2020-03-28T12:48:36.951Z"},"events":[],"suggestions":[]},{"id":671,"note":"Sit voluptatibus eveniet architecto quidem.","note_html":"<p>something else entirely</p>","cached_markdown_version":917504,"noteable_type":"MergeRequest","author_id":26,"created_at":"2016-06-14T15:02:56.632Z","updated_at":"2016-06-14T15:02:56.632Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[],"award_emoji":[{"id":1,"name":"tada","user_id":1,"awardable_type":"Note","awardable_id":1,"created_at":"2019-11-05T15:37:21.287Z","updated_at":"2019-11-05T15:37:21.287Z"}]},{"id":672,"note":"Odio maxime ratione voluptatibus sed.","noteable_type":"MergeRequest","author_id":25,"created_at":"2016-06-14T15:02:56.656Z","updated_at":"2016-06-14T15:02:56.656Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":673,"note":"Et deserunt et omnis nihil excepturi accusantium.","noteable_type":"MergeRequest","author_id":22,"created_at":"2016-06-14T15:02:56.679Z","updated_at":"2016-06-14T15:02:56.679Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":674,"note":"Saepe asperiores exercitationem non dignissimos laborum reiciendis et ipsum.","noteable_type":"MergeRequest","author_id":20,"created_at":"2016-06-14T15:02:56.700Z","updated_at":"2016-06-14T15:02:56.700Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[],"suggestions":[{"id":1,"note_id":674,"relative_order":0,"applied":false,"commit_id":null,"from_content":"Original line\n","to_content":"New line\n","lines_above":0,"lines_below":0,"outdated":false}]},{"id":675,"note":"Numquam est at dolor quo et sed eligendi similique.","noteable_type":"MergeRequest","author_id":16,"created_at":"2016-06-14T15:02:56.720Z","updated_at":"2016-06-14T15:02:56.720Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":676,"note":"Et perferendis aliquam sunt nisi labore delectus.","noteable_type":"MergeRequest","author_id":15,"created_at":"2016-06-14T15:02:56.742Z","updated_at":"2016-06-14T15:02:56.742Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":677,"note":"Aut ex rerum et in.","noteable_type":"MergeRequest","author_id":6,"created_at":"2016-06-14T15:02:56.791Z","updated_at":"2016-06-14T15:02:56.791Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":678,"note":"Dolor laborum earum ut exercitationem.","noteable_type":"MergeRequest","author_id":1,"created_at":"2016-06-14T15:02:56.814Z","updated_at":"2016-06-14T15:02:56.814Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"resource_label_events":[{"id":243,"action":"add","issue_id":null,"merge_request_id":27,"label_id":null,"user_id":1,"created_at":"2018-08-28T08:24:00.494Z"}],"merge_request_diff":{"id":27,"state":"collected","merge_request_diff_commits":[{"merge_request_diff_id":27,"relative_order":0,"sha":"bb5206fee213d983da88c47f9cf4cc6caf9c66dc","message":"Feature conflict added\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-08-06T08:35:52.000+02:00","committed_date":"2014-08-06T08:35:52.000+02:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":27,"relative_order":1,"sha":"5937ac0a7beb003549fc5fd26fc247adbce4a52e","message":"Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T10:01:38.000+01:00","committed_date":"2014-02-27T10:01:38.000+01:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":27,"relative_order":2,"sha":"570e7b2abdd848b95f2f578043fc23bd6f6fd24d","message":"Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:57:31.000+01:00","committed_date":"2014-02-27T09:57:31.000+01:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":27,"relative_order":3,"sha":"6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9","message":"More submodules\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:54:21.000+01:00","committed_date":"2014-02-27T09:54:21.000+01:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":27,"relative_order":4,"sha":"d14d6c0abdd253381df51a723d58691b2ee1ab08","message":"Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:49:50.000+01:00","committed_date":"2014-02-27T09:49:50.000+01:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":27,"relative_order":5,"sha":"c1acaa58bbcbc3eafe538cb8274ba387047b69f8","message":"Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:48:32.000+01:00","committed_date":"2014-02-27T09:48:32.000+01:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}}],"merge_request_diff_files":[{"merge_request_diff_id":27,"relative_order":0,"utf8_diff":"Binary files a/.DS_Store and /dev/null differ\n","new_path":".DS_Store","old_path":".DS_Store","a_mode":"100644","b_mode":"0","new_file":false,"renamed_file":false,"deleted_file":true,"too_large":false},{"merge_request_diff_id":27,"relative_order":1,"utf8_diff":"--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n","new_path":".gitignore","old_path":".gitignore","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":27,"relative_order":2,"utf8_diff":"--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n","new_path":".gitmodules","old_path":".gitmodules","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":27,"relative_order":3,"utf8_diff":"Binary files a/files/.DS_Store and /dev/null differ\n","new_path":"files/.DS_Store","old_path":"files/.DS_Store","a_mode":"100644","b_mode":"0","new_file":false,"renamed_file":false,"deleted_file":true,"too_large":false},{"merge_request_diff_id":27,"relative_order":4,"utf8_diff":"--- /dev/null\n+++ b/files/ruby/feature.rb\n@@ -0,0 +1,4 @@\n+# This file was changed in feature branch\n+# We put different code here to make merge conflict\n+class Conflict\n+end\n","new_path":"files/ruby/feature.rb","old_path":"files/ruby/feature.rb","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":27,"relative_order":5,"utf8_diff":"--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" => path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" => path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output << stdout.read\n @cmd_output << stderr.read\n","new_path":"files/ruby/popen.rb","old_path":"files/ruby/popen.rb","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":27,"relative_order":6,"utf8_diff":"--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n","new_path":"files/ruby/regex.rb","old_path":"files/ruby/regex.rb","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":27,"relative_order":7,"utf8_diff":"--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n","new_path":"gitlab-grack","old_path":"gitlab-grack","a_mode":"0","b_mode":"160000","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":27,"relative_order":8,"utf8_diff":"--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n","new_path":"gitlab-shell","old_path":"gitlab-shell","a_mode":"0","b_mode":"160000","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false}],"merge_request_id":27,"created_at":"2016-06-14T15:02:36.572Z","updated_at":"2016-06-14T15:02:36.658Z","base_commit_sha":"ae73cb07c9eeaf35924a10f713b364d32b2dd34f","real_size":"9"},"events":[{"id":221,"target_type":"MergeRequest","target_id":27,"project_id":36,"created_at":"2016-06-14T15:02:36.703Z","updated_at":"2016-06-14T15:02:36.703Z","action":1,"author_id":1},{"id":187,"target_type":"MergeRequest","target_id":27,"project_id":5,"created_at":"2016-06-14T15:02:36.703Z","updated_at":"2016-06-14T15:02:36.703Z","action":1,"author_id":1}],"approvals_before_merge":1,"award_emoji":[{"id":1,"name":"thumbsup","user_id":1,"awardable_type":"MergeRequest","awardable_id":27,"created_at":"2020-01-07T11:21:21.235Z","updated_at":"2020-01-07T11:21:21.235Z"},{"id":2,"name":"drum","user_id":1,"awardable_type":"MergeRequest","awardable_id":27,"created_at":"2020-01-07T11:21:21.235Z","updated_at":"2020-01-07T11:21:21.235Z"}]}
+{"id":26,"target_branch":"master","source_branch":"feature","source_project_id":4,"author_id":1,"assignee_id":null,"title":"MR2","created_at":"2016-06-14T15:02:36.418Z","updated_at":"2016-06-14T15:02:57.013Z","state":"opened","merge_status":"unchecked","target_project_id":5,"iid":8,"description":null,"position":0,"updated_by_id":null,"merge_error":null,"merge_params":{"force_remove_source_branch":null},"merge_when_pipeline_succeeds":false,"merge_user_id":null,"merge_commit_sha":null,"notes":[{"id":679,"note":"Qui rerum totam nisi est.","noteable_type":"MergeRequest","author_id":26,"created_at":"2016-06-14T15:02:56.848Z","updated_at":"2016-06-14T15:02:56.848Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":26,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":680,"note":"Pariatur magni corrupti consequatur debitis minima error beatae voluptatem.","noteable_type":"MergeRequest","author_id":25,"created_at":"2016-06-14T15:02:56.871Z","updated_at":"2016-06-14T15:02:56.871Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":26,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":681,"note":"Qui quis ut modi eos rerum ratione.","noteable_type":"MergeRequest","author_id":22,"created_at":"2016-06-14T15:02:56.895Z","updated_at":"2016-06-14T15:02:56.895Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":26,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":682,"note":"Illum quidem expedita mollitia fugit.","noteable_type":"MergeRequest","author_id":20,"created_at":"2016-06-14T15:02:56.918Z","updated_at":"2016-06-14T15:02:56.918Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":26,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":683,"note":"Consectetur voluptate sit sint possimus veritatis quod.","noteable_type":"MergeRequest","author_id":16,"created_at":"2016-06-14T15:02:56.942Z","updated_at":"2016-06-14T15:02:56.942Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":26,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":684,"note":"Natus libero quibusdam rem assumenda deleniti accusamus sed earum.","noteable_type":"MergeRequest","author_id":15,"created_at":"2016-06-14T15:02:56.966Z","updated_at":"2016-06-14T15:02:56.966Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":26,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":685,"note":"Tenetur autem nihil rerum odit.","noteable_type":"MergeRequest","author_id":6,"created_at":"2016-06-14T15:02:56.989Z","updated_at":"2016-06-14T15:02:56.989Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":26,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":686,"note":"Quia maiores et odio sed.","noteable_type":"MergeRequest","author_id":1,"created_at":"2016-06-14T15:02:57.012Z","updated_at":"2016-06-14T15:02:57.012Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":26,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"merge_request_diff":{"id":26,"state":"collected","merge_request_diff_commits":[{"merge_request_diff_id":26,"sha":"0b4bc9a49b562e85de7cc9e834518ea6828729b9","relative_order":0,"message":"Feature added\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:26:01.000+01:00","committed_date":"2014-02-27T09:26:01.000+01:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}}],"merge_request_diff_files":[{"merge_request_diff_id":26,"relative_order":0,"utf8_diff":"--- /dev/null\n+++ b/files/ruby/feature.rb\n@@ -0,0 +1,5 @@\n+class Feature\n+ def foo\n+ puts 'bar'\n+ end\n+end\n","new_path":"files/ruby/feature.rb","old_path":"files/ruby/feature.rb","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false}],"merge_request_id":26,"created_at":"2016-06-14T15:02:36.421Z","updated_at":"2016-06-14T15:02:36.474Z","base_commit_sha":"ae73cb07c9eeaf35924a10f713b364d32b2dd34f","real_size":"1"},"events":[{"id":222,"target_type":"MergeRequest","target_id":26,"project_id":36,"created_at":"2016-06-14T15:02:36.496Z","updated_at":"2016-06-14T15:02:36.496Z","action":1,"author_id":1},{"id":186,"target_type":"MergeRequest","target_id":26,"project_id":5,"created_at":"2016-06-14T15:02:36.496Z","updated_at":"2016-06-14T15:02:36.496Z","action":1,"author_id":1}]}
+{"id":15,"target_branch":"test-7","source_branch":"test-1","source_project_id":5,"author_id":22,"assignee_id":16,"title":"Qui accusantium et inventore facilis doloribus occaecati officiis.","created_at":"2016-06-14T15:02:25.168Z","updated_at":"2016-06-14T15:02:59.521Z","state":"opened","merge_status":"unchecked","target_project_id":5,"iid":7,"description":"Et commodi deserunt aspernatur vero rerum. Ut non dolorum alias in odit est libero. Voluptatibus eos in et vitae repudiandae facilis ex mollitia.","position":0,"updated_by_id":null,"merge_error":null,"merge_params":{"force_remove_source_branch":null},"merge_when_pipeline_succeeds":false,"merge_user_id":null,"merge_commit_sha":null,"notes":[{"id":777,"note":"Pariatur voluptas placeat aspernatur culpa suscipit soluta.","noteable_type":"MergeRequest","author_id":26,"created_at":"2016-06-14T15:02:59.348Z","updated_at":"2016-06-14T15:02:59.348Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":15,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":778,"note":"Alias et iure mollitia suscipit molestiae voluptatum nostrum asperiores.","noteable_type":"MergeRequest","author_id":25,"created_at":"2016-06-14T15:02:59.372Z","updated_at":"2016-06-14T15:02:59.372Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":15,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":779,"note":"Laudantium qui eum qui sunt.","noteable_type":"MergeRequest","author_id":22,"created_at":"2016-06-14T15:02:59.395Z","updated_at":"2016-06-14T15:02:59.395Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":15,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":780,"note":"Quas rem est iusto ut delectus fugiat recusandae mollitia.","noteable_type":"MergeRequest","author_id":20,"created_at":"2016-06-14T15:02:59.418Z","updated_at":"2016-06-14T15:02:59.418Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":15,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":781,"note":"Repellendus ab et qui nesciunt.","noteable_type":"MergeRequest","author_id":16,"created_at":"2016-06-14T15:02:59.444Z","updated_at":"2016-06-14T15:02:59.444Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":15,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":782,"note":"Non possimus voluptatum odio qui ut.","noteable_type":"MergeRequest","author_id":15,"created_at":"2016-06-14T15:02:59.469Z","updated_at":"2016-06-14T15:02:59.469Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":15,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":783,"note":"Dolores repellendus eum ducimus quam ab dolorem quia.","noteable_type":"MergeRequest","author_id":6,"created_at":"2016-06-14T15:02:59.494Z","updated_at":"2016-06-14T15:02:59.494Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":15,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":784,"note":"Facilis dolorem aut corrupti id ratione occaecati.","noteable_type":"MergeRequest","author_id":1,"created_at":"2016-06-14T15:02:59.520Z","updated_at":"2016-06-14T15:02:59.520Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":15,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"merge_request_diff":{"id":15,"state":"collected","merge_request_diff_commits":[{"merge_request_diff_id":15,"relative_order":0,"sha":"94b8d581c48d894b86661718582fecbc5e3ed2eb","message":"fixes #10\n","authored_date":"2016-01-19T13:22:56.000+01:00","committed_date":"2016-01-19T13:22:56.000+01:00","commit_author":{"name":"James Lopez","email":"james@jameslopez.es"},"committer":{"name":"James Lopez","email":"james@jameslopez.es"}}],"merge_request_diff_files":[{"merge_request_diff_id":15,"relative_order":0,"utf8_diff":"--- /dev/null\n+++ b/test\n","new_path":"test","old_path":"test","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false}],"merge_request_id":15,"created_at":"2016-06-14T15:02:25.171Z","updated_at":"2016-06-14T15:02:25.230Z","base_commit_sha":"be93687618e4b132087f430a4d8fc3a609c9b77c","real_size":"1"},"events":[{"id":223,"target_type":"MergeRequest","target_id":15,"project_id":36,"created_at":"2016-06-14T15:02:25.262Z","updated_at":"2016-06-14T15:02:25.262Z","action":1,"author_id":1},{"id":175,"target_type":"MergeRequest","target_id":15,"project_id":5,"created_at":"2016-06-14T15:02:25.262Z","updated_at":"2016-06-14T15:02:25.262Z","action":1,"author_id":22}]}
+{"id":14,"target_branch":"fix","source_branch":"test-3","source_project_id":5,"author_id":20,"assignee_id":20,"title":"In voluptas aut sequi voluptatem ullam vel corporis illum consequatur.","created_at":"2016-06-14T15:02:24.760Z","updated_at":"2016-06-14T15:02:59.749Z","state":"opened","merge_status":"unchecked","target_project_id":5,"iid":6,"description":"Dicta magnam non voluptates nam dignissimos nostrum deserunt. Dolorum et suscipit iure quae doloremque. Necessitatibus saepe aut labore sed.","position":0,"updated_by_id":null,"merge_error":null,"merge_params":{"force_remove_source_branch":null},"merge_when_pipeline_succeeds":false,"merge_user_id":null,"merge_commit_sha":null,"notes":[{"id":785,"note":"Atque cupiditate necessitatibus deserunt minus natus odit.","noteable_type":"MergeRequest","author_id":26,"created_at":"2016-06-14T15:02:59.559Z","updated_at":"2016-06-14T15:02:59.559Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":14,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":786,"note":"Non dolorem provident mollitia nesciunt optio ex eveniet.","noteable_type":"MergeRequest","author_id":25,"created_at":"2016-06-14T15:02:59.587Z","updated_at":"2016-06-14T15:02:59.587Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":14,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":787,"note":"Similique officia nemo quasi commodi accusantium quae qui.","noteable_type":"MergeRequest","author_id":22,"created_at":"2016-06-14T15:02:59.621Z","updated_at":"2016-06-14T15:02:59.621Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":14,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":788,"note":"Et est et alias ad dolor qui.","noteable_type":"MergeRequest","author_id":20,"created_at":"2016-06-14T15:02:59.650Z","updated_at":"2016-06-14T15:02:59.650Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":14,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":789,"note":"Numquam temporibus ratione voluptatibus aliquid.","noteable_type":"MergeRequest","author_id":16,"created_at":"2016-06-14T15:02:59.675Z","updated_at":"2016-06-14T15:02:59.675Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":14,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":790,"note":"Ut ex aliquam consectetur perferendis est hic aut quia.","noteable_type":"MergeRequest","author_id":15,"created_at":"2016-06-14T15:02:59.703Z","updated_at":"2016-06-14T15:02:59.703Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":14,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":791,"note":"Esse eos quam quaerat aut ut asperiores officiis.","noteable_type":"MergeRequest","author_id":6,"created_at":"2016-06-14T15:02:59.726Z","updated_at":"2016-06-14T15:02:59.726Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":14,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":792,"note":"Sint facilis accusantium iure blanditiis.","noteable_type":"MergeRequest","author_id":1,"created_at":"2016-06-14T15:02:59.748Z","updated_at":"2016-06-14T15:02:59.748Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":14,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"merge_request_diff":{"id":14,"state":"collected","merge_request_diff_commits":[{"merge_request_diff_id":14,"relative_order":0,"sha":"ddd4ff416a931589c695eb4f5b23f844426f6928","message":"fixes #10\n","authored_date":"2016-01-19T14:14:43.000+01:00","committed_date":"2016-01-19T14:14:43.000+01:00","commit_author":{"name":"James Lopez","email":"james@jameslopez.es"},"committer":{"name":"James Lopez","email":"james@jameslopez.es"}},{"merge_request_diff_id":14,"relative_order":1,"sha":"be93687618e4b132087f430a4d8fc3a609c9b77c","message":"Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6","authored_date":"2015-12-07T12:52:12.000+01:00","committed_date":"2015-12-07T12:52:12.000+01:00","commit_author":{"name":"Marin Jankovski","email":"marin@gitlab.com"},"committer":{"name":"Marin Jankovski","email":"marin@gitlab.com"}},{"merge_request_diff_id":14,"relative_order":2,"sha":"048721d90c449b244b7b4c53a9186b04330174ec","message":"LFS object pointer.\n","authored_date":"2015-12-07T11:54:28.000+01:00","committed_date":"2015-12-07T11:54:28.000+01:00","commit_author":{"name":"Marin Jankovski","email":"maxlazio@gmail.com"},"committer":{"name":"Marin Jankovski","email":"maxlazio@gmail.com"}},{"merge_request_diff_id":14,"relative_order":3,"sha":"5f923865dde3436854e9ceb9cdb7815618d4e849","message":"GitLab currently doesn't support patches that involve a merge commit: add a commit here\n","authored_date":"2015-11-13T16:27:12.000+01:00","committed_date":"2015-11-13T16:27:12.000+01:00","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":14,"relative_order":4,"sha":"d2d430676773caa88cdaf7c55944073b2fd5561a","message":"Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5","authored_date":"2015-11-13T08:50:17.000+01:00","committed_date":"2015-11-13T08:50:17.000+01:00","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":14,"relative_order":5,"sha":"2ea1f3dec713d940208fb5ce4a38765ecb5d3f73","message":"Add GitLab SVG\n","authored_date":"2015-11-13T08:39:43.000+01:00","committed_date":"2015-11-13T08:39:43.000+01:00","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":14,"relative_order":6,"sha":"59e29889be61e6e0e5e223bfa9ac2721d31605b8","message":"Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4","authored_date":"2015-11-13T07:21:40.000+01:00","committed_date":"2015-11-13T07:21:40.000+01:00","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":14,"relative_order":7,"sha":"66eceea0db202bb39c4e445e8ca28689645366c5","message":"add spaces in whitespace file\n","authored_date":"2015-11-13T06:01:27.000+01:00","committed_date":"2015-11-13T06:01:27.000+01:00","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":14,"relative_order":8,"sha":"08f22f255f082689c0d7d39d19205085311542bc","message":"remove empty file.(beacase git ignore empty file)\nadd whitespace test file.\n","authored_date":"2015-11-13T06:00:16.000+01:00","committed_date":"2015-11-13T06:00:16.000+01:00","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":14,"relative_order":9,"sha":"19e2e9b4ef76b422ce1154af39a91323ccc57434","message":"Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3","authored_date":"2015-11-13T05:23:14.000+01:00","committed_date":"2015-11-13T05:23:14.000+01:00","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":14,"relative_order":10,"sha":"c642fe9b8b9f28f9225d7ea953fe14e74748d53b","message":"add whitespace in empty\n","authored_date":"2015-11-13T05:08:45.000+01:00","committed_date":"2015-11-13T05:08:45.000+01:00","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":14,"relative_order":11,"sha":"9a944d90955aaf45f6d0c88f30e27f8d2c41cec0","message":"add empty file\n","authored_date":"2015-11-13T05:08:04.000+01:00","committed_date":"2015-11-13T05:08:04.000+01:00","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":14,"relative_order":12,"sha":"c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd","message":"Add ISO-8859 test file\n","authored_date":"2015-08-25T17:53:12.000+02:00","committed_date":"2015-08-25T17:53:12.000+02:00","commit_author":{"name":"Stan Hu","email":"stanhu@packetzoom.com"},"committer":{"name":"Stan Hu","email":"stanhu@packetzoom.com"}},{"merge_request_diff_id":14,"relative_order":13,"sha":"e56497bb5f03a90a51293fc6d516788730953899","message":"Merge branch 'tree_helper_spec' into 'master'\n\nAdd directory structure for tree_helper spec\n\nThis directory structure is needed for a testing the method flatten_tree(tree) in the TreeHelper module\n\nSee [merge request #275](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/275#note_732774)\n\nSee merge request !2\n","authored_date":"2015-01-10T22:23:29.000+01:00","committed_date":"2015-01-10T22:23:29.000+01:00","commit_author":{"name":"Sytse Sijbrandij","email":"sytse@gitlab.com"},"committer":{"name":"Sytse Sijbrandij","email":"sytse@gitlab.com"}},{"merge_request_diff_id":14,"relative_order":14,"sha":"4cd80ccab63c82b4bad16faa5193fbd2aa06df40","message":"add directory structure for tree_helper spec\n","authored_date":"2015-01-10T21:28:18.000+01:00","committed_date":"2015-01-10T21:28:18.000+01:00","commit_author":{"name":"marmis85","email":"marmis85@gmail.com"},"committer":{"name":"marmis85","email":"marmis85@gmail.com"}},{"merge_request_diff_id":14,"relative_order":15,"sha":"5937ac0a7beb003549fc5fd26fc247adbce4a52e","message":"Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T10:01:38.000+01:00","committed_date":"2014-02-27T10:01:38.000+01:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":14,"relative_order":16,"sha":"570e7b2abdd848b95f2f578043fc23bd6f6fd24d","message":"Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:57:31.000+01:00","committed_date":"2014-02-27T09:57:31.000+01:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":14,"relative_order":17,"sha":"6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9","message":"More submodules\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:54:21.000+01:00","committed_date":"2014-02-27T09:54:21.000+01:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":14,"relative_order":18,"sha":"d14d6c0abdd253381df51a723d58691b2ee1ab08","message":"Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:49:50.000+01:00","committed_date":"2014-02-27T09:49:50.000+01:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":14,"relative_order":19,"sha":"c1acaa58bbcbc3eafe538cb8274ba387047b69f8","message":"Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:48:32.000+01:00","committed_date":"2014-02-27T09:48:32.000+01:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}}],"merge_request_diff_files":[{"merge_request_diff_id":14,"relative_order":0,"utf8_diff":"Binary files a/.DS_Store and /dev/null differ\n","new_path":".DS_Store","old_path":".DS_Store","a_mode":"100644","b_mode":"0","new_file":false,"renamed_file":false,"deleted_file":true,"too_large":false},{"merge_request_diff_id":14,"relative_order":1,"utf8_diff":"--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n","new_path":".gitignore","old_path":".gitignore","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":2,"utf8_diff":"--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n","new_path":".gitmodules","old_path":".gitmodules","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":3,"utf8_diff":"--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n","new_path":"CHANGELOG","old_path":"CHANGELOG","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":4,"utf8_diff":"--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n","new_path":"encoding/iso8859.txt","old_path":"encoding/iso8859.txt","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":5,"utf8_diff":"Binary files a/files/.DS_Store and /dev/null differ\n","new_path":"files/.DS_Store","old_path":"files/.DS_Store","a_mode":"100644","b_mode":"0","new_file":false,"renamed_file":false,"deleted_file":true,"too_large":false},{"merge_request_diff_id":14,"relative_order":6,"utf8_diff":"--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n+<svg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\">\n+ <!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->\n+ <title>wm</title>\n+ <desc>Created with Sketch.</desc>\n+ <defs>\n+ <path id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"></path>\n+ </defs>\n+ <g id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\">\n+ <path d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\">\n+ <g id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\">\n+ <g id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\">\n+ <path d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"></path>\n+ </g>\n+ <g id=\"g16\">\n+ <g id=\"g18-Clipped\">\n+ <mask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\">\n+ <use xlink:href=\"#path-1\"></use>\n+ </mask>\n+ <g id=\"path22\"></g>\n+ <g id=\"g18\" mask=\"url(#mask-2)\">\n+ <g transform=\"translate(382.736659, 312.879425)\">\n+ <g id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\">\n+ <path d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\">\n+ <path d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\">\n+ <path d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\">\n+ <path d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <path d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <path d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\">\n+ <path d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\">\n+ <path d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path54\"></g>\n+ </g>\n+ <g id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\">\n+ <path d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <g id=\"path62\"></g>\n+ </g>\n+ <g id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\">\n+ <path d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path70\"></g>\n+ </g>\n+ <g id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\">\n+ <path d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <path d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\">\n+ <path d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\">\n+ <path d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+</svg>\n\\ No newline at end of file\n","new_path":"files/images/wm.svg","old_path":"files/images/wm.svg","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":7,"utf8_diff":"--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n","new_path":"files/lfs/lfs_object.iso","old_path":"files/lfs/lfs_object.iso","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":8,"utf8_diff":"--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" => path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" => path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output << stdout.read\n @cmd_output << stderr.read\n","new_path":"files/ruby/popen.rb","old_path":"files/ruby/popen.rb","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":9,"utf8_diff":"--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n","new_path":"files/ruby/regex.rb","old_path":"files/ruby/regex.rb","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":10,"utf8_diff":"--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n","new_path":"files/whitespace","old_path":"files/whitespace","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":11,"utf8_diff":"--- /dev/null\n+++ b/foo/bar/.gitkeep\n","new_path":"foo/bar/.gitkeep","old_path":"foo/bar/.gitkeep","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":12,"utf8_diff":"--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n","new_path":"gitlab-grack","old_path":"gitlab-grack","a_mode":"0","b_mode":"160000","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":13,"utf8_diff":"--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n","new_path":"gitlab-shell","old_path":"gitlab-shell","a_mode":"0","b_mode":"160000","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":14,"utf8_diff":"--- /dev/null\n+++ b/test\n","new_path":"test","old_path":"test","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false}],"merge_request_id":14,"created_at":"2016-06-14T15:02:24.770Z","updated_at":"2016-06-14T15:02:25.007Z","base_commit_sha":"ae73cb07c9eeaf35924a10f713b364d32b2dd34f","real_size":"15"},"events":[{"id":224,"target_type":"MergeRequest","target_id":14,"project_id":36,"created_at":"2016-06-14T15:02:25.113Z","updated_at":"2016-06-14T15:02:25.113Z","action":1,"author_id":1},{"id":174,"target_type":"MergeRequest","target_id":14,"project_id":5,"created_at":"2016-06-14T15:02:25.113Z","updated_at":"2016-06-14T15:02:25.113Z","action":1,"author_id":20}]}
+{"id":13,"target_branch":"improve/awesome","source_branch":"test-8","source_project_id":5,"author_id":16,"assignee_id":25,"title":"Voluptates consequatur eius nemo amet libero animi illum delectus tempore.","created_at":"2016-06-14T15:02:24.415Z","updated_at":"2016-06-14T15:02:59.958Z","state":"opened","merge_status":"unchecked","target_project_id":5,"iid":5,"description":"Est eaque quasi qui qui. Similique voluptatem impedit iusto ratione reprehenderit. Itaque est illum ut nulla aut.","position":0,"updated_by_id":null,"merge_error":null,"merge_params":{"force_remove_source_branch":null},"merge_when_pipeline_succeeds":false,"merge_user_id":null,"merge_commit_sha":null,"notes":[{"id":793,"note":"In illum maxime aperiam nulla est aspernatur.","noteable_type":"MergeRequest","author_id":26,"created_at":"2016-06-14T15:02:59.782Z","updated_at":"2016-06-14T15:02:59.782Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[{"merge_request_diff_id":14,"id":529,"target_type":"Note","target_id":793,"project_id":4,"created_at":"2016-07-07T14:35:12.128Z","updated_at":"2016-07-07T14:35:12.128Z","action":6,"author_id":1}]},{"id":794,"note":"Enim quia perferendis cum distinctio tenetur optio voluptas veniam.","noteable_type":"MergeRequest","author_id":25,"created_at":"2016-06-14T15:02:59.807Z","updated_at":"2016-06-14T15:02:59.807Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":795,"note":"Dolor ad quia quis pariatur ducimus.","noteable_type":"MergeRequest","author_id":22,"created_at":"2016-06-14T15:02:59.831Z","updated_at":"2016-06-14T15:02:59.831Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":796,"note":"Et a odio voluptate aut.","noteable_type":"MergeRequest","author_id":20,"created_at":"2016-06-14T15:02:59.854Z","updated_at":"2016-06-14T15:02:59.854Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":797,"note":"Quis nihil temporibus voluptatum modi minima a ut.","noteable_type":"MergeRequest","author_id":16,"created_at":"2016-06-14T15:02:59.879Z","updated_at":"2016-06-14T15:02:59.879Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":798,"note":"Ut alias consequatur in nostrum.","noteable_type":"MergeRequest","author_id":15,"created_at":"2016-06-14T15:02:59.904Z","updated_at":"2016-06-14T15:02:59.904Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":799,"note":"Voluptatibus aperiam assumenda et neque sint libero.","noteable_type":"MergeRequest","author_id":6,"created_at":"2016-06-14T15:02:59.926Z","updated_at":"2016-06-14T15:02:59.926Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":800,"note":"Veritatis voluptatem dolor dolores magni quo ut ipsa fuga.","noteable_type":"MergeRequest","author_id":1,"created_at":"2016-06-14T15:02:59.956Z","updated_at":"2016-06-14T15:02:59.956Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"merge_request_diff":{"id":13,"state":"collected","merge_request_diff_commits":[{"merge_request_diff_id":13,"relative_order":0,"sha":"0bfedc29d30280c7e8564e19f654584b459e5868","message":"fixes #10\n","authored_date":"2016-01-19T15:25:23.000+01:00","committed_date":"2016-01-19T15:25:23.000+01:00","commit_author":{"name":"James Lopez","email":"james@jameslopez.es"},"committer":{"name":"James Lopez","email":"james@jameslopez.es"}},{"merge_request_diff_id":13,"relative_order":1,"sha":"be93687618e4b132087f430a4d8fc3a609c9b77c","message":"Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6","authored_date":"2015-12-07T12:52:12.000+01:00","committed_date":"2015-12-07T12:52:12.000+01:00","commit_author":{"name":"Marin Jankovski","email":"marin@gitlab.com"},"committer":{"name":"Marin Jankovski","email":"marin@gitlab.com"}},{"merge_request_diff_id":13,"relative_order":2,"sha":"048721d90c449b244b7b4c53a9186b04330174ec","message":"LFS object pointer.\n","authored_date":"2015-12-07T11:54:28.000+01:00","committed_date":"2015-12-07T11:54:28.000+01:00","commit_author":{"name":"Marin Jankovski","email":"maxlazio@gmail.com"},"committer":{"name":"Marin Jankovski","email":"maxlazio@gmail.com"}},{"merge_request_diff_id":13,"relative_order":3,"sha":"5f923865dde3436854e9ceb9cdb7815618d4e849","message":"GitLab currently doesn't support patches that involve a merge commit: add a commit here\n","authored_date":"2015-11-13T16:27:12.000+01:00","committed_date":"2015-11-13T16:27:12.000+01:00","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":13,"relative_order":4,"sha":"d2d430676773caa88cdaf7c55944073b2fd5561a","message":"Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5","authored_date":"2015-11-13T08:50:17.000+01:00","committed_date":"2015-11-13T08:50:17.000+01:00","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":13,"relative_order":5,"sha":"2ea1f3dec713d940208fb5ce4a38765ecb5d3f73","message":"Add GitLab SVG\n","authored_date":"2015-11-13T08:39:43.000+01:00","committed_date":"2015-11-13T08:39:43.000+01:00","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":13,"relative_order":6,"sha":"59e29889be61e6e0e5e223bfa9ac2721d31605b8","message":"Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4","authored_date":"2015-11-13T07:21:40.000+01:00","committed_date":"2015-11-13T07:21:40.000+01:00","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":13,"relative_order":7,"sha":"66eceea0db202bb39c4e445e8ca28689645366c5","message":"add spaces in whitespace file\n","authored_date":"2015-11-13T06:01:27.000+01:00","committed_date":"2015-11-13T06:01:27.000+01:00","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":13,"relative_order":8,"sha":"08f22f255f082689c0d7d39d19205085311542bc","message":"remove empty file.(beacase git ignore empty file)\nadd whitespace test file.\n","authored_date":"2015-11-13T06:00:16.000+01:00","committed_date":"2015-11-13T06:00:16.000+01:00","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":13,"relative_order":9,"sha":"19e2e9b4ef76b422ce1154af39a91323ccc57434","message":"Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3","authored_date":"2015-11-13T05:23:14.000+01:00","committed_date":"2015-11-13T05:23:14.000+01:00","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":13,"relative_order":10,"sha":"c642fe9b8b9f28f9225d7ea953fe14e74748d53b","message":"add whitespace in empty\n","authored_date":"2015-11-13T05:08:45.000+01:00","committed_date":"2015-11-13T05:08:45.000+01:00","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":13,"relative_order":11,"sha":"9a944d90955aaf45f6d0c88f30e27f8d2c41cec0","message":"add empty file\n","authored_date":"2015-11-13T05:08:04.000+01:00","committed_date":"2015-11-13T05:08:04.000+01:00","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":13,"relative_order":12,"sha":"c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd","message":"Add ISO-8859 test file\n","authored_date":"2015-08-25T17:53:12.000+02:00","committed_date":"2015-08-25T17:53:12.000+02:00","commit_author":{"name":"Stan Hu","email":"stanhu@packetzoom.com"},"committer":{"name":"Stan Hu","email":"stanhu@packetzoom.com"}},{"merge_request_diff_id":13,"relative_order":13,"sha":"e56497bb5f03a90a51293fc6d516788730953899","message":"Merge branch 'tree_helper_spec' into 'master'\n\nAdd directory structure for tree_helper spec\n\nThis directory structure is needed for a testing the method flatten_tree(tree) in the TreeHelper module\n\nSee [merge request #275](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/275#note_732774)\n\nSee merge request !2\n","authored_date":"2015-01-10T22:23:29.000+01:00","committed_date":"2015-01-10T22:23:29.000+01:00","commit_author":{"name":"Sytse Sijbrandij","email":"sytse@gitlab.com"},"committer":{"name":"Sytse Sijbrandij","email":"sytse@gitlab.com"}},{"merge_request_diff_id":13,"relative_order":14,"sha":"4cd80ccab63c82b4bad16faa5193fbd2aa06df40","message":"add directory structure for tree_helper spec\n","authored_date":"2015-01-10T21:28:18.000+01:00","committed_date":"2015-01-10T21:28:18.000+01:00","commit_author":{"name":"marmis85","email":"marmis85@gmail.com"},"committer":{"name":"marmis85","email":"marmis85@gmail.com"}}],"merge_request_diff_files":[{"merge_request_diff_id":13,"relative_order":0,"utf8_diff":"--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n","new_path":"CHANGELOG","old_path":"CHANGELOG","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":13,"relative_order":1,"utf8_diff":"--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n","new_path":"encoding/iso8859.txt","old_path":"encoding/iso8859.txt","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":13,"relative_order":2,"utf8_diff":"--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n+<svg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\">\n+ <!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->\n+ <title>wm</title>\n+ <desc>Created with Sketch.</desc>\n+ <defs>\n+ <path id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"></path>\n+ </defs>\n+ <g id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\">\n+ <path d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\">\n+ <g id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\">\n+ <g id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\">\n+ <path d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"></path>\n+ </g>\n+ <g id=\"g16\">\n+ <g id=\"g18-Clipped\">\n+ <mask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\">\n+ <use xlink:href=\"#path-1\"></use>\n+ </mask>\n+ <g id=\"path22\"></g>\n+ <g id=\"g18\" mask=\"url(#mask-2)\">\n+ <g transform=\"translate(382.736659, 312.879425)\">\n+ <g id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\">\n+ <path d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\">\n+ <path d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\">\n+ <path d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\">\n+ <path d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <path d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <path d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\">\n+ <path d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\">\n+ <path d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path54\"></g>\n+ </g>\n+ <g id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\">\n+ <path d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <g id=\"path62\"></g>\n+ </g>\n+ <g id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\">\n+ <path d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path70\"></g>\n+ </g>\n+ <g id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\">\n+ <path d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <path d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\">\n+ <path d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\">\n+ <path d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+</svg>\n\\ No newline at end of file\n","new_path":"files/images/wm.svg","old_path":"files/images/wm.svg","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":13,"relative_order":3,"utf8_diff":"--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n","new_path":"files/lfs/lfs_object.iso","old_path":"files/lfs/lfs_object.iso","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":13,"relative_order":4,"utf8_diff":"--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n","new_path":"files/whitespace","old_path":"files/whitespace","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":13,"relative_order":5,"utf8_diff":"--- /dev/null\n+++ b/foo/bar/.gitkeep\n","new_path":"foo/bar/.gitkeep","old_path":"foo/bar/.gitkeep","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":13,"relative_order":6,"utf8_diff":"--- /dev/null\n+++ b/test\n","new_path":"test","old_path":"test","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false}],"merge_request_id":13,"created_at":"2016-06-14T15:02:24.420Z","updated_at":"2016-06-14T15:02:24.561Z","base_commit_sha":"5937ac0a7beb003549fc5fd26fc247adbce4a52e","real_size":"7"},"events":[{"id":225,"target_type":"MergeRequest","target_id":13,"project_id":36,"created_at":"2016-06-14T15:02:24.636Z","updated_at":"2016-06-14T15:02:24.636Z","action":1,"author_id":16},{"id":173,"target_type":"MergeRequest","target_id":13,"project_id":5,"created_at":"2016-06-14T15:02:24.636Z","updated_at":"2016-06-14T15:02:24.636Z","action":1,"author_id":16}]}
+{"id":12,"target_branch":"flatten-dirs","source_branch":"test-2","source_project_id":5,"author_id":1,"assignee_id":22,"title":"In a rerum harum nihil accusamus aut quia nobis non.","created_at":"2016-06-14T15:02:24.000Z","updated_at":"2016-06-14T15:03:00.225Z","state":"opened","merge_status":"unchecked","target_project_id":5,"iid":4,"description":"Nam magnam odit velit rerum. Sapiente dolore sunt saepe debitis. Culpa maiores ut ad dolores dolorem et.","position":0,"updated_by_id":null,"merge_error":null,"merge_params":{"force_remove_source_branch":null},"merge_when_pipeline_succeeds":false,"merge_user_id":null,"merge_commit_sha":null,"notes":[{"id":801,"note":"Nihil dicta molestias expedita atque.","noteable_type":"MergeRequest","author_id":26,"created_at":"2016-06-14T15:03:00.001Z","updated_at":"2016-06-14T15:03:00.001Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":12,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":802,"note":"Illum culpa voluptas enim accusantium deserunt.","noteable_type":"MergeRequest","author_id":25,"created_at":"2016-06-14T15:03:00.034Z","updated_at":"2016-06-14T15:03:00.034Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":12,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":803,"note":"Dicta esse aliquam laboriosam unde alias.","noteable_type":"MergeRequest","author_id":22,"created_at":"2016-06-14T15:03:00.065Z","updated_at":"2016-06-14T15:03:00.065Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":12,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":804,"note":"Dicta autem et sed molestiae ut quae.","noteable_type":"MergeRequest","author_id":20,"created_at":"2016-06-14T15:03:00.097Z","updated_at":"2016-06-14T15:03:00.097Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":12,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":805,"note":"Ut ut temporibus voluptas dolore quia velit.","noteable_type":"MergeRequest","author_id":16,"created_at":"2016-06-14T15:03:00.129Z","updated_at":"2016-06-14T15:03:00.129Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":12,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":806,"note":"Dolores similique sint pariatur error id quia fugit aut.","noteable_type":"MergeRequest","author_id":15,"created_at":"2016-06-14T15:03:00.162Z","updated_at":"2016-06-14T15:03:00.162Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":12,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":807,"note":"Quisquam provident nihil aperiam voluptatem.","noteable_type":"MergeRequest","author_id":6,"created_at":"2016-06-14T15:03:00.193Z","updated_at":"2016-06-14T15:03:00.193Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":12,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":808,"note":"Similique quo vero expedita deserunt ipsam earum.","noteable_type":"MergeRequest","author_id":1,"created_at":"2016-06-14T15:03:00.224Z","updated_at":"2016-06-14T15:03:00.224Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":12,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"merge_request_diff":{"id":12,"state":"collected","merge_request_diff_commits":[{"merge_request_diff_id":12,"relative_order":0,"sha":"97a0df9696e2aebf10c31b3016f40214e0e8f243","message":"fixes #10\n","authored_date":"2016-01-19T14:08:21.000+01:00","committed_date":"2016-01-19T14:08:21.000+01:00","commit_author":{"name":"James Lopez","email":"james@jameslopez.es"},"committer":{"name":"James Lopez","email":"james@jameslopez.es"}},{"merge_request_diff_id":12,"relative_order":1,"sha":"be93687618e4b132087f430a4d8fc3a609c9b77c","message":"Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6","authored_date":"2015-12-07T12:52:12.000+01:00","committed_date":"2015-12-07T12:52:12.000+01:00","commit_author":{"name":"Marin Jankovski","email":"marin@gitlab.com"},"committer":{"name":"Marin Jankovski","email":"marin@gitlab.com"}},{"merge_request_diff_id":12,"relative_order":2,"sha":"048721d90c449b244b7b4c53a9186b04330174ec","message":"LFS object pointer.\n","authored_date":"2015-12-07T11:54:28.000+01:00","committed_date":"2015-12-07T11:54:28.000+01:00","commit_author":{"name":"Marin Jankovski","email":"maxlazio@gmail.com"},"committer":{"name":"Marin Jankovski","email":"maxlazio@gmail.com"}},{"merge_request_diff_id":12,"relative_order":3,"sha":"5f923865dde3436854e9ceb9cdb7815618d4e849","message":"GitLab currently doesn't support patches that involve a merge commit: add a commit here\n","authored_date":"2015-11-13T16:27:12.000+01:00","committed_date":"2015-11-13T16:27:12.000+01:00","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":12,"relative_order":4,"sha":"d2d430676773caa88cdaf7c55944073b2fd5561a","message":"Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5","authored_date":"2015-11-13T08:50:17.000+01:00","committed_date":"2015-11-13T08:50:17.000+01:00","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":12,"relative_order":5,"sha":"2ea1f3dec713d940208fb5ce4a38765ecb5d3f73","message":"Add GitLab SVG\n","authored_date":"2015-11-13T08:39:43.000+01:00","committed_date":"2015-11-13T08:39:43.000+01:00","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":12,"relative_order":6,"sha":"59e29889be61e6e0e5e223bfa9ac2721d31605b8","message":"Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4","authored_date":"2015-11-13T07:21:40.000+01:00","committed_date":"2015-11-13T07:21:40.000+01:00","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":12,"relative_order":7,"sha":"66eceea0db202bb39c4e445e8ca28689645366c5","message":"add spaces in whitespace file\n","authored_date":"2015-11-13T06:01:27.000+01:00","committed_date":"2015-11-13T06:01:27.000+01:00","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":12,"relative_order":8,"sha":"08f22f255f082689c0d7d39d19205085311542bc","message":"remove empty file.(beacase git ignore empty file)\nadd whitespace test file.\n","authored_date":"2015-11-13T06:00:16.000+01:00","committed_date":"2015-11-13T06:00:16.000+01:00","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":12,"relative_order":9,"sha":"19e2e9b4ef76b422ce1154af39a91323ccc57434","message":"Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3","authored_date":"2015-11-13T05:23:14.000+01:00","committed_date":"2015-11-13T05:23:14.000+01:00","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":12,"relative_order":10,"sha":"c642fe9b8b9f28f9225d7ea953fe14e74748d53b","message":"add whitespace in empty\n","authored_date":"2015-11-13T05:08:45.000+01:00","committed_date":"2015-11-13T05:08:45.000+01:00","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":12,"relative_order":11,"sha":"9a944d90955aaf45f6d0c88f30e27f8d2c41cec0","message":"add empty file\n","authored_date":"2015-11-13T05:08:04.000+01:00","committed_date":"2015-11-13T05:08:04.000+01:00","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":12,"relative_order":12,"sha":"c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd","message":"Add ISO-8859 test file\n","authored_date":"2015-08-25T17:53:12.000+02:00","committed_date":"2015-08-25T17:53:12.000+02:00","commit_author":{"name":"Stan Hu","email":"stanhu@packetzoom.com"},"committer":{"name":"Stan Hu","email":"stanhu@packetzoom.com"}}],"merge_request_diff_files":[{"merge_request_diff_id":12,"relative_order":0,"utf8_diff":"--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n","new_path":"CHANGELOG","old_path":"CHANGELOG","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":12,"relative_order":1,"utf8_diff":"--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n","new_path":"encoding/iso8859.txt","old_path":"encoding/iso8859.txt","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":12,"relative_order":2,"utf8_diff":"--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n+<svg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\">\n+ <!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->\n+ <title>wm</title>\n+ <desc>Created with Sketch.</desc>\n+ <defs>\n+ <path id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"></path>\n+ </defs>\n+ <g id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\">\n+ <path d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\">\n+ <g id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\">\n+ <g id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\">\n+ <path d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"></path>\n+ </g>\n+ <g id=\"g16\">\n+ <g id=\"g18-Clipped\">\n+ <mask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\">\n+ <use xlink:href=\"#path-1\"></use>\n+ </mask>\n+ <g id=\"path22\"></g>\n+ <g id=\"g18\" mask=\"url(#mask-2)\">\n+ <g transform=\"translate(382.736659, 312.879425)\">\n+ <g id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\">\n+ <path d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\">\n+ <path d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\">\n+ <path d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\">\n+ <path d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <path d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <path d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\">\n+ <path d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\">\n+ <path d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path54\"></g>\n+ </g>\n+ <g id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\">\n+ <path d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <g id=\"path62\"></g>\n+ </g>\n+ <g id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\">\n+ <path d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path70\"></g>\n+ </g>\n+ <g id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\">\n+ <path d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <path d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\">\n+ <path d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\">\n+ <path d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+</svg>\n\\ No newline at end of file\n","new_path":"files/images/wm.svg","old_path":"files/images/wm.svg","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":12,"relative_order":3,"utf8_diff":"--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n","new_path":"files/lfs/lfs_object.iso","old_path":"files/lfs/lfs_object.iso","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":12,"relative_order":4,"utf8_diff":"--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n","new_path":"files/whitespace","old_path":"files/whitespace","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":12,"relative_order":5,"utf8_diff":"--- /dev/null\n+++ b/test\n","new_path":"test","old_path":"test","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false}],"merge_request_id":12,"created_at":"2016-06-14T15:02:24.006Z","updated_at":"2016-06-14T15:02:24.169Z","base_commit_sha":"e56497bb5f03a90a51293fc6d516788730953899","real_size":"6"},"events":[{"id":226,"target_type":"MergeRequest","target_id":12,"project_id":36,"created_at":"2016-06-14T15:02:24.253Z","updated_at":"2016-06-14T15:02:24.253Z","action":1,"author_id":1},{"id":172,"target_type":"MergeRequest","target_id":12,"project_id":5,"created_at":"2016-06-14T15:02:24.253Z","updated_at":"2016-06-14T15:02:24.253Z","action":1,"author_id":1}]}
{"id":11,"target_branch":"test-15","source_branch":"'test'","source_project_id":5,"author_id":16,"assignee_id":16,"title":"Corporis provident similique perspiciatis dolores eos animi.","created_at":"2016-06-14T15:02:23.767Z","updated_at":"2016-06-14T15:03:00.475Z","state":"opened","merge_status":"unchecked","target_project_id":5,"iid":3,"description":"Libero nesciunt mollitia quis odit eos vero quasi. Iure voluptatem ut sint pariatur voluptates ut aut. Laborum possimus unde illum ipsum eum.","position":0,"updated_by_id":null,"merge_error":null,"merge_params":{"force_remove_source_branch":null},"merge_when_pipeline_succeeds":false,"merge_user_id":null,"merge_commit_sha":null,"notes":[{"id":809,"note":"Omnis ratione laboriosam dolores qui.","noteable_type":"MergeRequest","author_id":26,"created_at":"2016-06-14T15:03:00.260Z","updated_at":"2016-06-14T15:03:00.260Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":11,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":810,"note":"Voluptas voluptates pariatur dolores maxime est voluptas.","noteable_type":"MergeRequest","author_id":25,"created_at":"2016-06-14T15:03:00.290Z","updated_at":"2016-06-14T15:03:00.290Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":11,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":811,"note":"Sit perspiciatis facilis ipsum consequatur.","noteable_type":"MergeRequest","author_id":22,"created_at":"2016-06-14T15:03:00.323Z","updated_at":"2016-06-14T15:03:00.323Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":11,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":812,"note":"Ut neque aliquam nam et est.","noteable_type":"MergeRequest","author_id":20,"created_at":"2016-06-14T15:03:00.349Z","updated_at":"2016-06-14T15:03:00.349Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":11,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":813,"note":"Et debitis rerum minima sit aut dolorem.","noteable_type":"MergeRequest","author_id":16,"created_at":"2016-06-14T15:03:00.374Z","updated_at":"2016-06-14T15:03:00.374Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":11,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":814,"note":"Ea nisi earum fugit iste aperiam consequatur.","noteable_type":"MergeRequest","author_id":15,"created_at":"2016-06-14T15:03:00.397Z","updated_at":"2016-06-14T15:03:00.397Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":11,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":815,"note":"Amet ratione consequatur laudantium rerum voluptas est nobis.","noteable_type":"MergeRequest","author_id":6,"created_at":"2016-06-14T15:03:00.450Z","updated_at":"2016-06-14T15:03:00.450Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":11,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":816,"note":"Ab ducimus cumque quia dolorem vitae sint beatae rerum.","noteable_type":"MergeRequest","author_id":1,"created_at":"2016-06-14T15:03:00.474Z","updated_at":"2016-06-14T15:03:00.474Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":11,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"merge_request_diff":{"id":11,"state":"empty","merge_request_diff_commits":[],"merge_request_diff_files":[],"merge_request_id":11,"created_at":"2016-06-14T15:02:23.772Z","updated_at":"2016-06-14T15:02:23.833Z","base_commit_sha":"e56497bb5f03a90a51293fc6d516788730953899","real_size":null},"events":[{"id":227,"target_type":"MergeRequest","target_id":11,"project_id":36,"created_at":"2016-06-14T15:02:23.865Z","updated_at":"2016-06-14T15:02:23.865Z","action":1,"author_id":16},{"id":171,"target_type":"MergeRequest","target_id":11,"project_id":5,"created_at":"2016-06-14T15:02:23.865Z","updated_at":"2016-06-14T15:02:23.865Z","action":1,"author_id":16}]}
-{"id":10,"target_branch":"feature","source_branch":"test-5","source_project_id":5,"author_id":20,"assignee_id":25,"title":"Eligendi reprehenderit doloribus quia et sit id.","created_at":"2016-06-14T15:02:23.014Z","updated_at":"2016-06-14T15:03:00.685Z","state":"opened","merge_status":"unchecked","target_project_id":5,"iid":2,"description":"Ut dolor quia aliquid dolore et nisi. Est minus suscipit enim quaerat sapiente consequatur rerum. Eveniet provident consequatur dolor accusantium reiciendis.","position":0,"updated_by_id":null,"merge_error":null,"merge_params":{"force_remove_source_branch":null},"merge_when_pipeline_succeeds":false,"merge_user_id":null,"merge_commit_sha":null,"notes":[{"id":817,"note":"Recusandae et voluptas enim qui et.","noteable_type":"MergeRequest","author_id":26,"created_at":"2016-06-14T15:03:00.510Z","updated_at":"2016-06-14T15:03:00.510Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":10,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":818,"note":"Asperiores dolorem rerum ipsum totam.","noteable_type":"MergeRequest","author_id":25,"created_at":"2016-06-14T15:03:00.538Z","updated_at":"2016-06-14T15:03:00.538Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":10,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":819,"note":"Qui quam et iure quasi provident cumque itaque sequi.","noteable_type":"MergeRequest","author_id":22,"created_at":"2016-06-14T15:03:00.562Z","updated_at":"2016-06-14T15:03:00.562Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":10,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":820,"note":"Sint accusantium aliquid iste qui iusto minus vel.","noteable_type":"MergeRequest","author_id":20,"created_at":"2016-06-14T15:03:00.585Z","updated_at":"2016-06-14T15:03:00.585Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":10,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":821,"note":"Dolor corrupti dolorem blanditiis voluptas.","noteable_type":"MergeRequest","author_id":16,"created_at":"2016-06-14T15:03:00.610Z","updated_at":"2016-06-14T15:03:00.610Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":10,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":822,"note":"Est perferendis assumenda aliquam aliquid sit ipsum ullam aut.","noteable_type":"MergeRequest","author_id":15,"created_at":"2016-06-14T15:03:00.635Z","updated_at":"2016-06-14T15:03:00.635Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":10,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":823,"note":"Hic neque reiciendis quaerat maiores.","noteable_type":"MergeRequest","author_id":6,"created_at":"2016-06-14T15:03:00.659Z","updated_at":"2016-06-14T15:03:00.659Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":10,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":824,"note":"Sequi architecto doloribus ut vel autem.","noteable_type":"MergeRequest","author_id":1,"created_at":"2016-06-14T15:03:00.683Z","updated_at":"2016-06-14T15:03:00.683Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":10,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"merge_request_diff":{"id":10,"state":"collected","merge_request_diff_commits":[{"merge_request_diff_id":10,"relative_order":0,"sha":"f998ac87ac9244f15e9c15109a6f4e62a54b779d","message":"fixes #10\n","authored_date":"2016-01-19T14:43:23.000+01:00","author_name":"James Lopez","author_email":"james@jameslopez.es","committed_date":"2016-01-19T14:43:23.000+01:00","committer_name":"James Lopez","committer_email":"james@jameslopez.es","commit_author":{"name":"James Lopez","email":"james@jameslopez.es"},"committer":{"name":"James Lopez","email":"james@jameslopez.es"}},{"merge_request_diff_id":10,"relative_order":1,"sha":"be93687618e4b132087f430a4d8fc3a609c9b77c","message":"Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6","authored_date":"2015-12-07T12:52:12.000+01:00","author_name":"Marin Jankovski","author_email":"marin@gitlab.com","committed_date":"2015-12-07T12:52:12.000+01:00","committer_name":"Marin Jankovski","committer_email":"marin@gitlab.com","commit_author":{"name":"Marin Jankovski","email":"marin@gitlab.com"},"committer":{"name":"Marin Jankovski","email":"marin@gitlab.com"}},{"merge_request_diff_id":10,"relative_order":2,"sha":"048721d90c449b244b7b4c53a9186b04330174ec","message":"LFS object pointer.\n","authored_date":"2015-12-07T11:54:28.000+01:00","author_name":"Marin Jankovski","author_email":"maxlazio@gmail.com","committed_date":"2015-12-07T11:54:28.000+01:00","committer_name":"Marin Jankovski","committer_email":"maxlazio@gmail.com","commit_author":{"name":"Marin Jankovski","email":"maxlazio@gmail.com"},"committer":{"name":"Marin Jankovski","email":"maxlazio@gmail.com"}},{"merge_request_diff_id":10,"relative_order":3,"sha":"5f923865dde3436854e9ceb9cdb7815618d4e849","message":"GitLab currently doesn't support patches that involve a merge commit: add a commit here\n","authored_date":"2015-11-13T16:27:12.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T16:27:12.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":10,"relative_order":4,"sha":"d2d430676773caa88cdaf7c55944073b2fd5561a","message":"Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5","authored_date":"2015-11-13T08:50:17.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T08:50:17.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":10,"relative_order":5,"sha":"2ea1f3dec713d940208fb5ce4a38765ecb5d3f73","message":"Add GitLab SVG\n","authored_date":"2015-11-13T08:39:43.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T08:39:43.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":10,"relative_order":6,"sha":"59e29889be61e6e0e5e223bfa9ac2721d31605b8","message":"Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4","authored_date":"2015-11-13T07:21:40.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T07:21:40.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":10,"relative_order":7,"sha":"66eceea0db202bb39c4e445e8ca28689645366c5","message":"add spaces in whitespace file\n","authored_date":"2015-11-13T06:01:27.000+01:00","author_name":"윤민식","author_email":"minsik.yoon@samsung.com","committed_date":"2015-11-13T06:01:27.000+01:00","committer_name":"윤민식","committer_email":"minsik.yoon@samsung.com","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":10,"relative_order":8,"sha":"08f22f255f082689c0d7d39d19205085311542bc","message":"remove empty file.(beacase git ignore empty file)\nadd whitespace test file.\n","authored_date":"2015-11-13T06:00:16.000+01:00","author_name":"윤민식","author_email":"minsik.yoon@samsung.com","committed_date":"2015-11-13T06:00:16.000+01:00","committer_name":"윤민식","committer_email":"minsik.yoon@samsung.com","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":10,"relative_order":9,"sha":"19e2e9b4ef76b422ce1154af39a91323ccc57434","message":"Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3","authored_date":"2015-11-13T05:23:14.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T05:23:14.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":10,"relative_order":10,"sha":"c642fe9b8b9f28f9225d7ea953fe14e74748d53b","message":"add whitespace in empty\n","authored_date":"2015-11-13T05:08:45.000+01:00","author_name":"윤민식","author_email":"minsik.yoon@samsung.com","committed_date":"2015-11-13T05:08:45.000+01:00","committer_name":"윤민식","committer_email":"minsik.yoon@samsung.com","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":10,"relative_order":11,"sha":"9a944d90955aaf45f6d0c88f30e27f8d2c41cec0","message":"add empty file\n","authored_date":"2015-11-13T05:08:04.000+01:00","author_name":"윤민식","author_email":"minsik.yoon@samsung.com","committed_date":"2015-11-13T05:08:04.000+01:00","committer_name":"윤민식","committer_email":"minsik.yoon@samsung.com","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":10,"relative_order":12,"sha":"c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd","message":"Add ISO-8859 test file\n","authored_date":"2015-08-25T17:53:12.000+02:00","author_name":"Stan Hu","author_email":"stanhu@packetzoom.com","committed_date":"2015-08-25T17:53:12.000+02:00","committer_name":"Stan Hu","committer_email":"stanhu@packetzoom.com","commit_author":{"name":"Stan Hu","email":"stanhu@packetzoom.com"},"committer":{"name":"Stan Hu","email":"stanhu@packetzoom.com"}},{"merge_request_diff_id":10,"relative_order":13,"sha":"e56497bb5f03a90a51293fc6d516788730953899","message":"Merge branch 'tree_helper_spec' into 'master'\n\nAdd directory structure for tree_helper spec\n\nThis directory structure is needed for a testing the method flatten_tree(tree) in the TreeHelper module\n\nSee [merge request #275](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/275#note_732774)\n\nSee merge request !2\n","authored_date":"2015-01-10T22:23:29.000+01:00","author_name":"Sytse Sijbrandij","author_email":"sytse@gitlab.com","committed_date":"2015-01-10T22:23:29.000+01:00","committer_name":"Sytse Sijbrandij","committer_email":"sytse@gitlab.com","commit_author":{"name":"Sytse Sijbrandij","email":"sytse@gitlab.com"},"committer":{"name":"Sytse Sijbrandij","email":"sytse@gitlab.com"}},{"merge_request_diff_id":10,"relative_order":14,"sha":"4cd80ccab63c82b4bad16faa5193fbd2aa06df40","message":"add directory structure for tree_helper spec\n","authored_date":"2015-01-10T21:28:18.000+01:00","author_name":"marmis85","author_email":"marmis85@gmail.com","committed_date":"2015-01-10T21:28:18.000+01:00","committer_name":"marmis85","committer_email":"marmis85@gmail.com","commit_author":{"name":"marmis85","email":"marmis85@gmail.com"},"committer":{"name":"marmis85","email":"marmis85@gmail.com"}},{"merge_request_diff_id":10,"relative_order":16,"sha":"5937ac0a7beb003549fc5fd26fc247adbce4a52e","message":"Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T10:01:38.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T10:01:38.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":10,"relative_order":17,"sha":"570e7b2abdd848b95f2f578043fc23bd6f6fd24d","message":"Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:57:31.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T09:57:31.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":10,"relative_order":18,"sha":"6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9","message":"More submodules\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:54:21.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T09:54:21.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":10,"relative_order":19,"sha":"d14d6c0abdd253381df51a723d58691b2ee1ab08","message":"Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:49:50.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T09:49:50.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":10,"relative_order":20,"sha":"c1acaa58bbcbc3eafe538cb8274ba387047b69f8","message":"Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:48:32.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T09:48:32.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}}],"merge_request_diff_files":[{"merge_request_diff_id":10,"relative_order":0,"utf8_diff":"Binary files a/.DS_Store and /dev/null differ\n","new_path":".DS_Store","old_path":".DS_Store","a_mode":"100644","b_mode":"0","new_file":false,"renamed_file":false,"deleted_file":true,"too_large":false},{"merge_request_diff_id":10,"relative_order":1,"utf8_diff":"--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n","new_path":".gitignore","old_path":".gitignore","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":2,"utf8_diff":"--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n","new_path":".gitmodules","old_path":".gitmodules","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":3,"utf8_diff":"--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n","new_path":"CHANGELOG","old_path":"CHANGELOG","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":4,"utf8_diff":"--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n","new_path":"encoding/iso8859.txt","old_path":"encoding/iso8859.txt","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":5,"utf8_diff":"Binary files a/files/.DS_Store and /dev/null differ\n","new_path":"files/.DS_Store","old_path":"files/.DS_Store","a_mode":"100644","b_mode":"0","new_file":false,"renamed_file":false,"deleted_file":true,"too_large":false},{"merge_request_diff_id":10,"relative_order":6,"utf8_diff":"--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n+<svg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\">\n+ <!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->\n+ <title>wm</title>\n+ <desc>Created with Sketch.</desc>\n+ <defs>\n+ <path id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"></path>\n+ </defs>\n+ <g id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\">\n+ <path d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\">\n+ <g id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\">\n+ <g id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\">\n+ <path d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"></path>\n+ </g>\n+ <g id=\"g16\">\n+ <g id=\"g18-Clipped\">\n+ <mask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\">\n+ <use xlink:href=\"#path-1\"></use>\n+ </mask>\n+ <g id=\"path22\"></g>\n+ <g id=\"g18\" mask=\"url(#mask-2)\">\n+ <g transform=\"translate(382.736659, 312.879425)\">\n+ <g id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\">\n+ <path d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\">\n+ <path d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\">\n+ <path d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\">\n+ <path d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <path d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <path d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\">\n+ <path d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\">\n+ <path d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path54\"></g>\n+ </g>\n+ <g id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\">\n+ <path d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <g id=\"path62\"></g>\n+ </g>\n+ <g id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\">\n+ <path d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path70\"></g>\n+ </g>\n+ <g id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\">\n+ <path d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <path d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\">\n+ <path d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\">\n+ <path d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+</svg>\n\\ No newline at end of file\n","new_path":"files/images/wm.svg","old_path":"files/images/wm.svg","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":7,"utf8_diff":"--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n","new_path":"files/lfs/lfs_object.iso","old_path":"files/lfs/lfs_object.iso","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":8,"utf8_diff":"--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" => path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" => path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output << stdout.read\n @cmd_output << stderr.read\n","new_path":"files/ruby/popen.rb","old_path":"files/ruby/popen.rb","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":9,"utf8_diff":"--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n","new_path":"files/ruby/regex.rb","old_path":"files/ruby/regex.rb","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":10,"utf8_diff":"--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n","new_path":"files/whitespace","old_path":"files/whitespace","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":11,"utf8_diff":"--- /dev/null\n+++ b/foo/bar/.gitkeep\n","new_path":"foo/bar/.gitkeep","old_path":"foo/bar/.gitkeep","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":12,"utf8_diff":"--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n","new_path":"gitlab-grack","old_path":"gitlab-grack","a_mode":"0","b_mode":"160000","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":13,"utf8_diff":"--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n","new_path":"gitlab-shell","old_path":"gitlab-shell","a_mode":"0","b_mode":"160000","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":14,"utf8_diff":"--- /dev/null\n+++ b/test\n","new_path":"test","old_path":"test","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false}],"merge_request_id":10,"created_at":"2016-06-14T15:02:23.019Z","updated_at":"2016-06-14T15:02:23.493Z","base_commit_sha":"ae73cb07c9eeaf35924a10f713b364d32b2dd34f","real_size":"15"},"events":[{"id":228,"target_type":"MergeRequest","target_id":10,"project_id":36,"created_at":"2016-06-14T15:02:23.660Z","updated_at":"2016-06-14T15:02:23.660Z","action":1,"author_id":1},{"id":170,"target_type":"MergeRequest","target_id":10,"project_id":5,"created_at":"2016-06-14T15:02:23.660Z","updated_at":"2016-06-14T15:02:23.660Z","action":1,"author_id":20}]}
-{"id":9,"target_branch":"test-6","source_branch":"test-12","source_project_id":5,"author_id":16,"assignee_id":6,"title":"Et ipsam voluptas velit sequi illum ut.","created_at":"2016-06-14T15:02:22.825Z","updated_at":"2016-06-14T15:03:00.904Z","state":"opened","merge_status":"unchecked","target_project_id":5,"iid":1,"description":"Eveniet nihil ratione veniam similique qui aut sapiente tempora. Sed praesentium iusto dignissimos possimus id repudiandae quo nihil. Qui doloremque autem et iure fugit.","position":0,"updated_by_id":null,"merge_error":null,"merge_params":{"force_remove_source_branch":null},"merge_when_pipeline_succeeds":false,"merge_user_id":null,"merge_commit_sha":null,"notes":[{"id":825,"note":"Aliquid voluptatem consequatur voluptas ex perspiciatis.","noteable_type":"MergeRequest","author_id":26,"created_at":"2016-06-14T15:03:00.722Z","updated_at":"2016-06-14T15:03:00.722Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":9,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":826,"note":"Itaque optio voluptatem praesentium voluptas.","noteable_type":"MergeRequest","author_id":25,"created_at":"2016-06-14T15:03:00.745Z","updated_at":"2016-06-14T15:03:00.745Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":9,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":827,"note":"Ut est corporis fuga asperiores delectus excepturi aperiam.","noteable_type":"MergeRequest","author_id":22,"created_at":"2016-06-14T15:03:00.771Z","updated_at":"2016-06-14T15:03:00.771Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":9,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":828,"note":"Similique ea dolore officiis temporibus.","noteable_type":"MergeRequest","author_id":20,"created_at":"2016-06-14T15:03:00.798Z","updated_at":"2016-06-14T15:03:00.798Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":9,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":829,"note":"Qui laudantium qui quae quis.","noteable_type":"MergeRequest","author_id":16,"created_at":"2016-06-14T15:03:00.828Z","updated_at":"2016-06-14T15:03:00.828Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":9,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":830,"note":"Et vel voluptas amet laborum qui soluta.","noteable_type":"MergeRequest","author_id":15,"created_at":"2016-06-14T15:03:00.850Z","updated_at":"2016-06-14T15:03:00.850Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":9,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":831,"note":"Enim ad consequuntur assumenda provident voluptatem similique deleniti.","noteable_type":"MergeRequest","author_id":6,"created_at":"2016-06-14T15:03:00.876Z","updated_at":"2016-06-14T15:03:00.876Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":9,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":832,"note":"Officiis sequi commodi pariatur totam fugiat voluptas corporis dignissimos.","noteable_type":"MergeRequest","author_id":1,"created_at":"2016-06-14T15:03:00.902Z","updated_at":"2016-06-14T15:03:00.902Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":9,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"merge_request_diff":{"id":9,"state":"collected","merge_request_diff_commits":[{"merge_request_diff_id":9,"relative_order":0,"sha":"a4e5dfebf42e34596526acb8611bc7ed80e4eb3f","message":"fixes #10\n","authored_date":"2016-01-19T15:44:02.000+01:00","author_name":"James Lopez","author_email":"james@jameslopez.es","committed_date":"2016-01-19T15:44:02.000+01:00","committer_name":"James Lopez","committer_email":"james@jameslopez.es","commit_author":{"name":"James Lopez","email":"james@jameslopez.es"},"committer":{"name":"James Lopez","email":"james@jameslopez.es"}}],"merge_request_diff_files":[{"merge_request_diff_id":9,"relative_order":0,"utf8_diff":"--- /dev/null\n+++ b/test\n","new_path":"test","old_path":"test","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false}],"merge_request_id":9,"created_at":"2016-06-14T15:02:22.829Z","updated_at":"2016-06-14T15:02:22.900Z","base_commit_sha":"be93687618e4b132087f430a4d8fc3a609c9b77c","real_size":"1"},"events":[{"id":229,"target_type":"MergeRequest","target_id":9,"project_id":36,"created_at":"2016-06-14T15:02:22.927Z","updated_at":"2016-06-14T15:02:22.927Z","action":1,"author_id":16},{"id":169,"target_type":"MergeRequest","target_id":9,"project_id":5,"created_at":"2016-06-14T15:02:22.927Z","updated_at":"2016-06-14T15:02:22.927Z","action":1,"author_id":16}]}
+{"id":10,"target_branch":"feature","source_branch":"test-5","source_project_id":5,"author_id":20,"assignee_id":25,"title":"Eligendi reprehenderit doloribus quia et sit id.","created_at":"2016-06-14T15:02:23.014Z","updated_at":"2016-06-14T15:03:00.685Z","state":"opened","merge_status":"unchecked","target_project_id":5,"iid":2,"description":"Ut dolor quia aliquid dolore et nisi. Est minus suscipit enim quaerat sapiente consequatur rerum. Eveniet provident consequatur dolor accusantium reiciendis.","position":0,"updated_by_id":null,"merge_error":null,"merge_params":{"force_remove_source_branch":null},"merge_when_pipeline_succeeds":false,"merge_user_id":null,"merge_commit_sha":null,"notes":[{"id":817,"note":"Recusandae et voluptas enim qui et.","noteable_type":"MergeRequest","author_id":26,"created_at":"2016-06-14T15:03:00.510Z","updated_at":"2016-06-14T15:03:00.510Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":10,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":818,"note":"Asperiores dolorem rerum ipsum totam.","noteable_type":"MergeRequest","author_id":25,"created_at":"2016-06-14T15:03:00.538Z","updated_at":"2016-06-14T15:03:00.538Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":10,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":819,"note":"Qui quam et iure quasi provident cumque itaque sequi.","noteable_type":"MergeRequest","author_id":22,"created_at":"2016-06-14T15:03:00.562Z","updated_at":"2016-06-14T15:03:00.562Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":10,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":820,"note":"Sint accusantium aliquid iste qui iusto minus vel.","noteable_type":"MergeRequest","author_id":20,"created_at":"2016-06-14T15:03:00.585Z","updated_at":"2016-06-14T15:03:00.585Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":10,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":821,"note":"Dolor corrupti dolorem blanditiis voluptas.","noteable_type":"MergeRequest","author_id":16,"created_at":"2016-06-14T15:03:00.610Z","updated_at":"2016-06-14T15:03:00.610Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":10,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":822,"note":"Est perferendis assumenda aliquam aliquid sit ipsum ullam aut.","noteable_type":"MergeRequest","author_id":15,"created_at":"2016-06-14T15:03:00.635Z","updated_at":"2016-06-14T15:03:00.635Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":10,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":823,"note":"Hic neque reiciendis quaerat maiores.","noteable_type":"MergeRequest","author_id":6,"created_at":"2016-06-14T15:03:00.659Z","updated_at":"2016-06-14T15:03:00.659Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":10,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":824,"note":"Sequi architecto doloribus ut vel autem.","noteable_type":"MergeRequest","author_id":1,"created_at":"2016-06-14T15:03:00.683Z","updated_at":"2016-06-14T15:03:00.683Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":10,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"merge_request_diff":{"id":10,"state":"collected","merge_request_diff_commits":[{"merge_request_diff_id":10,"relative_order":0,"sha":"f998ac87ac9244f15e9c15109a6f4e62a54b779d","message":"fixes #10\n","authored_date":"2016-01-19T14:43:23.000+01:00","committed_date":"2016-01-19T14:43:23.000+01:00","commit_author":{"name":"James Lopez","email":"james@jameslopez.es"},"committer":{"name":"James Lopez","email":"james@jameslopez.es"}},{"merge_request_diff_id":10,"relative_order":1,"sha":"be93687618e4b132087f430a4d8fc3a609c9b77c","message":"Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6","authored_date":"2015-12-07T12:52:12.000+01:00","committed_date":"2015-12-07T12:52:12.000+01:00","commit_author":{"name":"Marin Jankovski","email":"marin@gitlab.com"},"committer":{"name":"Marin Jankovski","email":"marin@gitlab.com"}},{"merge_request_diff_id":10,"relative_order":2,"sha":"048721d90c449b244b7b4c53a9186b04330174ec","message":"LFS object pointer.\n","authored_date":"2015-12-07T11:54:28.000+01:00","committed_date":"2015-12-07T11:54:28.000+01:00","commit_author":{"name":"Marin Jankovski","email":"maxlazio@gmail.com"},"committer":{"name":"Marin Jankovski","email":"maxlazio@gmail.com"}},{"merge_request_diff_id":10,"relative_order":3,"sha":"5f923865dde3436854e9ceb9cdb7815618d4e849","message":"GitLab currently doesn't support patches that involve a merge commit: add a commit here\n","authored_date":"2015-11-13T16:27:12.000+01:00","committed_date":"2015-11-13T16:27:12.000+01:00","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":10,"relative_order":4,"sha":"d2d430676773caa88cdaf7c55944073b2fd5561a","message":"Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5","authored_date":"2015-11-13T08:50:17.000+01:00","committed_date":"2015-11-13T08:50:17.000+01:00","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":10,"relative_order":5,"sha":"2ea1f3dec713d940208fb5ce4a38765ecb5d3f73","message":"Add GitLab SVG\n","authored_date":"2015-11-13T08:39:43.000+01:00","committed_date":"2015-11-13T08:39:43.000+01:00","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":10,"relative_order":6,"sha":"59e29889be61e6e0e5e223bfa9ac2721d31605b8","message":"Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4","authored_date":"2015-11-13T07:21:40.000+01:00","committed_date":"2015-11-13T07:21:40.000+01:00","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":10,"relative_order":7,"sha":"66eceea0db202bb39c4e445e8ca28689645366c5","message":"add spaces in whitespace file\n","authored_date":"2015-11-13T06:01:27.000+01:00","committed_date":"2015-11-13T06:01:27.000+01:00","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":10,"relative_order":8,"sha":"08f22f255f082689c0d7d39d19205085311542bc","message":"remove empty file.(beacase git ignore empty file)\nadd whitespace test file.\n","authored_date":"2015-11-13T06:00:16.000+01:00","committed_date":"2015-11-13T06:00:16.000+01:00","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":10,"relative_order":9,"sha":"19e2e9b4ef76b422ce1154af39a91323ccc57434","message":"Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3","authored_date":"2015-11-13T05:23:14.000+01:00","committed_date":"2015-11-13T05:23:14.000+01:00","commit_author":{"name":"Stan Hu","email":"stanhu@gmail.com"},"committer":{"name":"Stan Hu","email":"stanhu@gmail.com"}},{"merge_request_diff_id":10,"relative_order":10,"sha":"c642fe9b8b9f28f9225d7ea953fe14e74748d53b","message":"add whitespace in empty\n","authored_date":"2015-11-13T05:08:45.000+01:00","committed_date":"2015-11-13T05:08:45.000+01:00","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":10,"relative_order":11,"sha":"9a944d90955aaf45f6d0c88f30e27f8d2c41cec0","message":"add empty file\n","authored_date":"2015-11-13T05:08:04.000+01:00","committed_date":"2015-11-13T05:08:04.000+01:00","commit_author":{"name":"윤민식","email":"minsik.yoon@samsung.com"},"committer":{"name":"윤민식","email":"minsik.yoon@samsung.com"}},{"merge_request_diff_id":10,"relative_order":12,"sha":"c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd","message":"Add ISO-8859 test file\n","authored_date":"2015-08-25T17:53:12.000+02:00","committed_date":"2015-08-25T17:53:12.000+02:00","commit_author":{"name":"Stan Hu","email":"stanhu@packetzoom.com"},"committer":{"name":"Stan Hu","email":"stanhu@packetzoom.com"}},{"merge_request_diff_id":10,"relative_order":13,"sha":"e56497bb5f03a90a51293fc6d516788730953899","message":"Merge branch 'tree_helper_spec' into 'master'\n\nAdd directory structure for tree_helper spec\n\nThis directory structure is needed for a testing the method flatten_tree(tree) in the TreeHelper module\n\nSee [merge request #275](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/275#note_732774)\n\nSee merge request !2\n","authored_date":"2015-01-10T22:23:29.000+01:00","committed_date":"2015-01-10T22:23:29.000+01:00","commit_author":{"name":"Sytse Sijbrandij","email":"sytse@gitlab.com"},"committer":{"name":"Sytse Sijbrandij","email":"sytse@gitlab.com"}},{"merge_request_diff_id":10,"relative_order":14,"sha":"4cd80ccab63c82b4bad16faa5193fbd2aa06df40","message":"add directory structure for tree_helper spec\n","authored_date":"2015-01-10T21:28:18.000+01:00","committed_date":"2015-01-10T21:28:18.000+01:00","commit_author":{"name":"marmis85","email":"marmis85@gmail.com"},"committer":{"name":"marmis85","email":"marmis85@gmail.com"}},{"merge_request_diff_id":10,"relative_order":16,"sha":"5937ac0a7beb003549fc5fd26fc247adbce4a52e","message":"Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T10:01:38.000+01:00","committed_date":"2014-02-27T10:01:38.000+01:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":10,"relative_order":17,"sha":"570e7b2abdd848b95f2f578043fc23bd6f6fd24d","message":"Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:57:31.000+01:00","committed_date":"2014-02-27T09:57:31.000+01:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":10,"relative_order":18,"sha":"6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9","message":"More submodules\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:54:21.000+01:00","committed_date":"2014-02-27T09:54:21.000+01:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":10,"relative_order":19,"sha":"d14d6c0abdd253381df51a723d58691b2ee1ab08","message":"Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:49:50.000+01:00","committed_date":"2014-02-27T09:49:50.000+01:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}},{"merge_request_diff_id":10,"relative_order":20,"sha":"c1acaa58bbcbc3eafe538cb8274ba387047b69f8","message":"Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:48:32.000+01:00","committed_date":"2014-02-27T09:48:32.000+01:00","commit_author":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"},"committer":{"name":"Dmitriy Zaporozhets","email":"dmitriy.zaporozhets@gmail.com"}}],"merge_request_diff_files":[{"merge_request_diff_id":10,"relative_order":0,"utf8_diff":"Binary files a/.DS_Store and /dev/null differ\n","new_path":".DS_Store","old_path":".DS_Store","a_mode":"100644","b_mode":"0","new_file":false,"renamed_file":false,"deleted_file":true,"too_large":false},{"merge_request_diff_id":10,"relative_order":1,"utf8_diff":"--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n","new_path":".gitignore","old_path":".gitignore","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":2,"utf8_diff":"--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n","new_path":".gitmodules","old_path":".gitmodules","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":3,"utf8_diff":"--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n","new_path":"CHANGELOG","old_path":"CHANGELOG","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":4,"utf8_diff":"--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n","new_path":"encoding/iso8859.txt","old_path":"encoding/iso8859.txt","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":5,"utf8_diff":"Binary files a/files/.DS_Store and /dev/null differ\n","new_path":"files/.DS_Store","old_path":"files/.DS_Store","a_mode":"100644","b_mode":"0","new_file":false,"renamed_file":false,"deleted_file":true,"too_large":false},{"merge_request_diff_id":10,"relative_order":6,"utf8_diff":"--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n+<svg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\">\n+ <!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->\n+ <title>wm</title>\n+ <desc>Created with Sketch.</desc>\n+ <defs>\n+ <path id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"></path>\n+ </defs>\n+ <g id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\">\n+ <path d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\">\n+ <g id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\">\n+ <g id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\">\n+ <path d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"></path>\n+ </g>\n+ <g id=\"g16\">\n+ <g id=\"g18-Clipped\">\n+ <mask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\">\n+ <use xlink:href=\"#path-1\"></use>\n+ </mask>\n+ <g id=\"path22\"></g>\n+ <g id=\"g18\" mask=\"url(#mask-2)\">\n+ <g transform=\"translate(382.736659, 312.879425)\">\n+ <g id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\">\n+ <path d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\">\n+ <path d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\">\n+ <path d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\">\n+ <path d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <path d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <path d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\">\n+ <path d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\">\n+ <path d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path54\"></g>\n+ </g>\n+ <g id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\">\n+ <path d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <g id=\"path62\"></g>\n+ </g>\n+ <g id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\">\n+ <path d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path70\"></g>\n+ </g>\n+ <g id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\">\n+ <path d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <path d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\">\n+ <path d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\">\n+ <path d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+</svg>\n\\ No newline at end of file\n","new_path":"files/images/wm.svg","old_path":"files/images/wm.svg","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":7,"utf8_diff":"--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n","new_path":"files/lfs/lfs_object.iso","old_path":"files/lfs/lfs_object.iso","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":8,"utf8_diff":"--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" => path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" => path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output << stdout.read\n @cmd_output << stderr.read\n","new_path":"files/ruby/popen.rb","old_path":"files/ruby/popen.rb","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":9,"utf8_diff":"--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n","new_path":"files/ruby/regex.rb","old_path":"files/ruby/regex.rb","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":10,"utf8_diff":"--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n","new_path":"files/whitespace","old_path":"files/whitespace","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":11,"utf8_diff":"--- /dev/null\n+++ b/foo/bar/.gitkeep\n","new_path":"foo/bar/.gitkeep","old_path":"foo/bar/.gitkeep","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":12,"utf8_diff":"--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n","new_path":"gitlab-grack","old_path":"gitlab-grack","a_mode":"0","b_mode":"160000","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":13,"utf8_diff":"--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n","new_path":"gitlab-shell","old_path":"gitlab-shell","a_mode":"0","b_mode":"160000","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":14,"utf8_diff":"--- /dev/null\n+++ b/test\n","new_path":"test","old_path":"test","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false}],"merge_request_id":10,"created_at":"2016-06-14T15:02:23.019Z","updated_at":"2016-06-14T15:02:23.493Z","base_commit_sha":"ae73cb07c9eeaf35924a10f713b364d32b2dd34f","real_size":"15"},"events":[{"id":228,"target_type":"MergeRequest","target_id":10,"project_id":36,"created_at":"2016-06-14T15:02:23.660Z","updated_at":"2016-06-14T15:02:23.660Z","action":1,"author_id":1},{"id":170,"target_type":"MergeRequest","target_id":10,"project_id":5,"created_at":"2016-06-14T15:02:23.660Z","updated_at":"2016-06-14T15:02:23.660Z","action":1,"author_id":20}]}
+{"id":9,"target_branch":"test-6","source_branch":"test-12","source_project_id":5,"author_id":16,"assignee_id":6,"title":"Et ipsam voluptas velit sequi illum ut.","created_at":"2016-06-14T15:02:22.825Z","updated_at":"2016-06-14T15:03:00.904Z","state":"opened","merge_status":"unchecked","target_project_id":5,"iid":1,"description":"Eveniet nihil ratione veniam similique qui aut sapiente tempora. Sed praesentium iusto dignissimos possimus id repudiandae quo nihil. Qui doloremque autem et iure fugit.","position":0,"updated_by_id":null,"merge_error":null,"merge_params":{"force_remove_source_branch":null},"merge_when_pipeline_succeeds":false,"merge_user_id":null,"merge_commit_sha":null,"notes":[{"id":825,"note":"Aliquid voluptatem consequatur voluptas ex perspiciatis.","noteable_type":"MergeRequest","author_id":26,"created_at":"2016-06-14T15:03:00.722Z","updated_at":"2016-06-14T15:03:00.722Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":9,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":826,"note":"Itaque optio voluptatem praesentium voluptas.","noteable_type":"MergeRequest","author_id":25,"created_at":"2016-06-14T15:03:00.745Z","updated_at":"2016-06-14T15:03:00.745Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":9,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":827,"note":"Ut est corporis fuga asperiores delectus excepturi aperiam.","noteable_type":"MergeRequest","author_id":22,"created_at":"2016-06-14T15:03:00.771Z","updated_at":"2016-06-14T15:03:00.771Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":9,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":828,"note":"Similique ea dolore officiis temporibus.","noteable_type":"MergeRequest","author_id":20,"created_at":"2016-06-14T15:03:00.798Z","updated_at":"2016-06-14T15:03:00.798Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":9,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":829,"note":"Qui laudantium qui quae quis.","noteable_type":"MergeRequest","author_id":16,"created_at":"2016-06-14T15:03:00.828Z","updated_at":"2016-06-14T15:03:00.828Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":9,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":830,"note":"Et vel voluptas amet laborum qui soluta.","noteable_type":"MergeRequest","author_id":15,"created_at":"2016-06-14T15:03:00.850Z","updated_at":"2016-06-14T15:03:00.850Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":9,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":831,"note":"Enim ad consequuntur assumenda provident voluptatem similique deleniti.","noteable_type":"MergeRequest","author_id":6,"created_at":"2016-06-14T15:03:00.876Z","updated_at":"2016-06-14T15:03:00.876Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":9,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":832,"note":"Officiis sequi commodi pariatur totam fugiat voluptas corporis dignissimos.","noteable_type":"MergeRequest","author_id":1,"created_at":"2016-06-14T15:03:00.902Z","updated_at":"2016-06-14T15:03:00.902Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":9,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"merge_request_diff":{"id":9,"state":"collected","merge_request_diff_commits":[{"merge_request_diff_id":9,"relative_order":0,"sha":"a4e5dfebf42e34596526acb8611bc7ed80e4eb3f","message":"fixes #10\n","authored_date":"2016-01-19T15:44:02.000+01:00","committed_date":"2016-01-19T15:44:02.000+01:00","commit_author":{"name":"James Lopez","email":"james@jameslopez.es"},"committer":{"name":"James Lopez","email":"james@jameslopez.es"}}],"merge_request_diff_files":[{"merge_request_diff_id":9,"relative_order":0,"utf8_diff":"--- /dev/null\n+++ b/test\n","new_path":"test","old_path":"test","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false}],"merge_request_id":9,"created_at":"2016-06-14T15:02:22.829Z","updated_at":"2016-06-14T15:02:22.900Z","base_commit_sha":"be93687618e4b132087f430a4d8fc3a609c9b77c","real_size":"1"},"events":[{"id":229,"target_type":"MergeRequest","target_id":9,"project_id":36,"created_at":"2016-06-14T15:02:22.927Z","updated_at":"2016-06-14T15:02:22.927Z","action":1,"author_id":16},{"id":169,"target_type":"MergeRequest","target_id":9,"project_id":5,"created_at":"2016-06-14T15:02:22.927Z","updated_at":"2016-06-14T15:02:22.927Z","action":1,"author_id":16}]}
diff --git a/spec/fixtures/packages/npm/payload.json b/spec/fixtures/packages/npm/payload.json
index 664aa636001..5ecb013b9bf 100644
--- a/spec/fixtures/packages/npm/payload.json
+++ b/spec/fixtures/packages/npm/payload.json
@@ -14,7 +14,8 @@
"express":"^4.16.4"
},
"dist":{
- "shasum":"f572d396fae9206628714fb2ce00f72e94f2258f"
+ "shasum":"f572d396fae9206628714fb2ce00f72e94f2258f",
+ "tarball":"http://localhost/npm/package.tgz"
}
}
},
diff --git a/spec/fixtures/packages/npm/payload_with_duplicated_packages.json b/spec/fixtures/packages/npm/payload_with_duplicated_packages.json
index a6ea8760bd5..bc4a7b3f55a 100644
--- a/spec/fixtures/packages/npm/payload_with_duplicated_packages.json
+++ b/spec/fixtures/packages/npm/payload_with_duplicated_packages.json
@@ -28,7 +28,8 @@
"express":"^4.16.4"
},
"dist":{
- "shasum":"f572d396fae9206628714fb2ce00f72e94f2258f"
+ "shasum":"f572d396fae9206628714fb2ce00f72e94f2258f",
+ "tarball":"http://localhost/npm/package.tgz"
}
}
},
diff --git a/spec/fixtures/scripts/test_report.json b/spec/fixtures/scripts/test_report.json
new file mode 100644
index 00000000000..29fd9a4bcb5
--- /dev/null
+++ b/spec/fixtures/scripts/test_report.json
@@ -0,0 +1,36 @@
+{
+ "suites": [
+ {
+ "name": "rspec unit pg12",
+ "total_time": 975.6635620000018,
+ "total_count": 3811,
+ "success_count": 3800,
+ "failed_count": 1,
+ "skipped_count": 10,
+ "error_count": 0,
+ "suite_error": null,
+ "test_cases": [
+ {
+ "status": "failed",
+ "name": "Note associations is expected not to belong to project required: ",
+ "classname": "spec.models.note_spec",
+ "file": "./spec/models/note_spec.rb",
+ "execution_time": 0.209091,
+ "system_output": "Failure/Error: it { is_expected.not_to belong_to(:project) }\n Did not expect Note to have a belongs_to association called project\n./spec/models/note_spec.rb:9:in `block (3 levels) in <top (required)>'\n./spec/spec_helper.rb:392:in `block (3 levels) in <top (required)>'\n./spec/support/sidekiq_middleware.rb:9:in `with_sidekiq_server_middleware'\n./spec/spec_helper.rb:383:in `block (2 levels) in <top (required)>'\n./spec/spec_helper.rb:379:in `block (3 levels) in <top (required)>'\n./lib/gitlab/application_context.rb:31:in `with_raw_context'\n./spec/spec_helper.rb:379:in `block (2 levels) in <top (required)>'\n./spec/support/database/prevent_cross_joins.rb:95:in `block (3 levels) in <top (required)>'\n./spec/support/database/prevent_cross_joins.rb:62:in `with_cross_joins_prevented'\n./spec/support/database/prevent_cross_joins.rb:95:in `block (2 levels) in <top (required)>'",
+ "stack_trace": null,
+ "recent_failures": null
+ },
+ {
+ "status": "success",
+ "name": "Gitlab::ImportExport yields the initial tree when importing and exporting it again",
+ "classname": "spec.lib.gitlab.import_export.import_export_equivalence_spec",
+ "file": "./spec/lib/gitlab/import_export/import_export_equivalence_spec.rb",
+ "execution_time": 17.084198,
+ "system_output": null,
+ "stack_trace": null,
+ "recent_failures": null
+ }
+ ]
+ }
+ ]
+}
diff --git a/spec/frontend/__helpers__/experimentation_helper.js b/spec/frontend/__helpers__/experimentation_helper.js
index 7a2ef61216a..e0156226acc 100644
--- a/spec/frontend/__helpers__/experimentation_helper.js
+++ b/spec/frontend/__helpers__/experimentation_helper.js
@@ -1,5 +1,6 @@
import { merge } from 'lodash';
+// This helper is for specs that use `gitlab/experimentation` module
export function withGonExperiment(experimentKey, value = true) {
let origGon;
@@ -12,16 +13,26 @@ export function withGonExperiment(experimentKey, value = true) {
window.gon = origGon;
});
}
-// This helper is for specs that use `gitlab-experiment` utilities, which have a different schema that gets pushed via Gon compared to `Experimentation Module`
-export function assignGitlabExperiment(experimentKey, variant) {
- let origGon;
- beforeEach(() => {
- origGon = window.gon;
- window.gon = { experiment: { [experimentKey]: { variant } } };
- });
+// The following helper is for specs that use `gitlab-experiment` utilities,
+// which have a different schema that gets pushed to the frontend compared to
+// the `Experimentation` Module.
+//
+// Usage: stubExperiments({ experiment_feature_flag_name: 'variant_name', ... })
+export function stubExperiments(experiments = {}) {
+ // Deprecated
+ window.gon = window.gon || {};
+ window.gon.experiment = window.gon.experiment || {};
+ // Preferred
+ window.gl = window.gl || {};
+ window.gl.experiments = window.gl.experiemnts || {};
- afterEach(() => {
- window.gon = origGon;
+ Object.entries(experiments).forEach(([name, variant]) => {
+ const experimentData = { experiment: name, variant };
+
+ // Deprecated
+ window.gon.experiment[name] = experimentData;
+ // Preferred
+ window.gl.experiments[name] = experimentData;
});
}
diff --git a/spec/frontend/__mocks__/@gitlab/ui.js b/spec/frontend/__mocks__/@gitlab/ui.js
index 4c491a87fcb..6b3f1f01e6a 100644
--- a/spec/frontend/__mocks__/@gitlab/ui.js
+++ b/spec/frontend/__mocks__/@gitlab/ui.js
@@ -14,7 +14,9 @@ export * from '@gitlab/ui';
*/
jest.mock('@gitlab/ui/dist/directives/tooltip.js', () => ({
- bind() {},
+ GlTooltipDirective: {
+ bind() {},
+ },
}));
jest.mock('@gitlab/ui/dist/components/base/tooltip/tooltip.js', () => ({
diff --git a/spec/frontend/admin/analytics/devops_score/components/devops_score_callout_spec.js b/spec/frontend/admin/analytics/devops_score/components/devops_score_callout_spec.js
index ee14e002f1b..c9a899ab78b 100644
--- a/spec/frontend/admin/analytics/devops_score/components/devops_score_callout_spec.js
+++ b/spec/frontend/admin/analytics/devops_score/components/devops_score_callout_spec.js
@@ -1,7 +1,7 @@
import { GlBanner } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import DevopsScoreCallout from '~/analytics/devops_report/components/devops_score_callout.vue';
-import { INTRO_COOKIE_KEY } from '~/analytics/devops_report/constants';
+import DevopsScoreCallout from '~/analytics/devops_reports/components/devops_score_callout.vue';
+import { INTRO_COOKIE_KEY } from '~/analytics/devops_reports/constants';
import * as utils from '~/lib/utils/common_utils';
import { devopsReportDocsPath, devopsScoreIntroImagePath } from '../mock_data';
diff --git a/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js b/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js
index 8f8dac977de..824eb033671 100644
--- a/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js
+++ b/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js
@@ -2,8 +2,8 @@ import { GlTable, GlBadge, GlEmptyState } from '@gitlab/ui';
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import DevopsScore from '~/analytics/devops_report/components/devops_score.vue';
-import DevopsScoreCallout from '~/analytics/devops_report/components/devops_score_callout.vue';
+import DevopsScore from '~/analytics/devops_reports/components/devops_score.vue';
+import DevopsScoreCallout from '~/analytics/devops_reports/components/devops_score_callout.vue';
import { devopsScoreMetricsData, noDataImagePath, devopsScoreTableHeaders } from '../mock_data';
describe('DevopsScore', () => {
diff --git a/spec/frontend/admin/deploy_keys/components/table_spec.js b/spec/frontend/admin/deploy_keys/components/table_spec.js
new file mode 100644
index 00000000000..3b3be488043
--- /dev/null
+++ b/spec/frontend/admin/deploy_keys/components/table_spec.js
@@ -0,0 +1,47 @@
+import { merge } from 'lodash';
+import { GlTable, GlButton } from '@gitlab/ui';
+
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import DeployKeysTable from '~/admin/deploy_keys/components/table.vue';
+
+describe('DeployKeysTable', () => {
+ let wrapper;
+
+ const defaultProvide = {
+ createPath: '/admin/deploy_keys/new',
+ deletePath: '/admin/deploy_keys/:id',
+ editPath: '/admin/deploy_keys/:id/edit',
+ emptyStateSvgPath: '/assets/illustrations/empty-state/empty-deploy-keys.svg',
+ };
+
+ const createComponent = (provide = {}) => {
+ wrapper = mountExtended(DeployKeysTable, {
+ provide: merge({}, defaultProvide, provide),
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders page title', () => {
+ createComponent();
+
+ expect(wrapper.findByText(DeployKeysTable.i18n.pageTitle).exists()).toBe(true);
+ });
+
+ it('renders table', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlTable).exists()).toBe(true);
+ });
+
+ it('renders `New deploy key` button', () => {
+ createComponent();
+
+ const newDeployKeyButton = wrapper.findComponent(GlButton);
+
+ expect(newDeployKeyButton.text()).toBe(DeployKeysTable.i18n.newDeployKeyButtonText);
+ expect(newDeployKeyButton.attributes('href')).toBe(defaultProvide.createPath);
+ });
+});
diff --git a/spec/frontend/alert_handler_spec.js b/spec/frontend/alert_handler_spec.js
index e4cd38a7799..228053b1b2b 100644
--- a/spec/frontend/alert_handler_spec.js
+++ b/spec/frontend/alert_handler_spec.js
@@ -26,12 +26,12 @@ describe('Alert Handler', () => {
});
it('should render the alert', () => {
- expect(findFirstAlert()).toExist();
+ expect(findFirstAlert()).not.toBe(null);
});
it('should dismiss the alert on click', () => {
findFirstDismissButton().click();
- expect(findFirstAlert()).not.toExist();
+ expect(findFirstAlert()).toBe(null);
});
});
@@ -58,12 +58,12 @@ describe('Alert Handler', () => {
});
it('should render the banner', () => {
- expect(findFirstBanner()).toExist();
+ expect(findFirstBanner()).not.toBe(null);
});
it('should dismiss the banner on click', () => {
findFirstDismissButton().click();
- expect(findFirstBanner()).not.toExist();
+ expect(findFirstBanner()).toBe(null);
});
});
@@ -79,12 +79,12 @@ describe('Alert Handler', () => {
});
it('should render the banner', () => {
- expect(findFirstAlert()).toExist();
+ expect(findFirstAlert()).not.toBe(null);
});
it('should dismiss the banner on click', () => {
findFirstDismissButtonByClass().click();
- expect(findFirstAlert()).not.toExist();
+ expect(findFirstAlert()).toBe(null);
});
});
});
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 20e8bc059ec..39aab8dc1f8 100644
--- a/spec/frontend/alert_management/components/alert_management_table_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_table_spec.js
@@ -40,7 +40,6 @@ describe('AlertManagementTable', () => {
resolved: 11,
all: 26,
};
- const findDeprecationNotice = () => wrapper.findByTestId('alerts-deprecation-warning');
function mountComponent({ provide = {}, data = {}, loading = false, stubs = {} } = {}) {
wrapper = extendedWrapper(
@@ -49,7 +48,6 @@ describe('AlertManagementTable', () => {
...defaultProvideValues,
alertManagementEnabled: true,
userCanEnableAlertManagement: true,
- hasManagedPrometheus: false,
...provide,
},
data() {
@@ -237,22 +235,6 @@ describe('AlertManagementTable', () => {
expect(visitUrl).toHaveBeenCalledWith('/1527542/details', true);
});
- it.each`
- managedAlertsDeprecation | hasManagedPrometheus | isVisible
- ${false} | ${false} | ${false}
- ${false} | ${true} | ${true}
- ${true} | ${false} | ${false}
- ${true} | ${true} | ${false}
- `(
- 'when the deprecation feature flag is $managedAlertsDeprecation and has managed prometheus is $hasManagedPrometheus',
- ({ hasManagedPrometheus, managedAlertsDeprecation, isVisible }) => {
- mountComponent({
- provide: { hasManagedPrometheus, glFeatures: { managedAlertsDeprecation } },
- });
- expect(findDeprecationNotice().exists()).toBe(isVisible);
- },
- );
-
describe('alert issue links', () => {
beforeEach(() => {
mountComponent({
diff --git a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
index 298596085ef..bdc1dde7d48 100644
--- a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
@@ -1,4 +1,12 @@
-import { GlForm, GlFormSelect, GlFormInput, GlToggle, GlFormTextarea, GlTab } from '@gitlab/ui';
+import {
+ GlForm,
+ GlFormSelect,
+ GlFormInput,
+ GlToggle,
+ GlFormTextarea,
+ GlTab,
+ GlLink,
+} from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
@@ -58,7 +66,6 @@ describe('AlertsSettingsForm', () => {
afterEach(() => {
if (wrapper) {
wrapper.destroy();
- wrapper = null;
}
});
@@ -69,7 +76,7 @@ describe('AlertsSettingsForm', () => {
const enableIntegration = (index, value) => {
findFormFields().at(index).setValue(value);
- findFormToggle().trigger('click');
+ findFormToggle().vm.$emit('change', true);
};
describe('with default values', () => {
@@ -102,6 +109,12 @@ describe('AlertsSettingsForm', () => {
expect(findFormFields().at(0).attributes('id')).not.toBe('name-integration');
});
+ it('verify pricing link url', () => {
+ createComponent({ props: { canAddIntegration: false } });
+ const link = findMultiSupportText().findComponent(GlLink);
+ expect(link.attributes('href')).toMatch(/https:\/\/about.gitlab.(com|cn)\/pricing/);
+ });
+
describe('form tabs', () => {
it('renders 3 tabs', () => {
expect(findTabs()).toHaveLength(3);
diff --git a/spec/frontend/analytics/devops_report/components/service_ping_disabled_spec.js b/spec/frontend/analytics/devops_reports/components/service_ping_disabled_spec.js
index c5c40e9a360..c62bfb11f7b 100644
--- a/spec/frontend/analytics/devops_report/components/service_ping_disabled_spec.js
+++ b/spec/frontend/analytics/devops_reports/components/service_ping_disabled_spec.js
@@ -1,9 +1,9 @@
import { GlEmptyState, GlSprintf } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import ServicePingDisabled from '~/analytics/devops_report/components/service_ping_disabled.vue';
+import ServicePingDisabled from '~/analytics/devops_reports/components/service_ping_disabled.vue';
-describe('~/analytics/devops_report/components/service_ping_disabled.vue', () => {
+describe('~/analytics/devops_reports/components/service_ping_disabled.vue', () => {
let wrapper;
afterEach(() => {
diff --git a/spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js b/spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js
index 870375318e3..694c16a85c4 100644
--- a/spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js
+++ b/spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js
@@ -1,7 +1,6 @@
-import { within } from '@testing-library/dom';
-import { GlForm } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { GlForm, GlModal } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
import ManageTwoFactorForm, {
i18n,
} from '~/authentication/two_factor_auth/components/manage_two_factor_form.vue';
@@ -17,100 +16,133 @@ describe('ManageTwoFactorForm', () => {
let wrapper;
const createComponent = (options = {}) => {
- wrapper = extendedWrapper(
- mount(ManageTwoFactorForm, {
- provide: {
- ...defaultProvide,
- webauthnEnabled: options?.webauthnEnabled ?? false,
- isCurrentPasswordRequired: options?.currentPasswordRequired ?? true,
- },
- }),
- );
+ wrapper = mountExtended(ManageTwoFactorForm, {
+ provide: {
+ ...defaultProvide,
+ webauthnEnabled: options?.webauthnEnabled ?? false,
+ isCurrentPasswordRequired: options?.currentPasswordRequired ?? true,
+ },
+ stubs: {
+ GlModal: stubComponent(GlModal, {
+ template: `
+ <div>
+ <slot name="modal-title"></slot>
+ <slot></slot>
+ <slot name="modal-footer"></slot>
+ </div>`,
+ }),
+ },
+ });
};
- const queryByText = (text, options) => within(wrapper.element).queryByText(text, options);
- const queryByLabelText = (text, options) =>
- within(wrapper.element).queryByLabelText(text, options);
-
const findForm = () => wrapper.findComponent(GlForm);
const findMethodInput = () => wrapper.findByTestId('test-2fa-method-field');
const findDisableButton = () => wrapper.findByTestId('test-2fa-disable-button');
const findRegenerateCodesButton = () => wrapper.findByTestId('test-2fa-regenerate-codes-button');
+ const findConfirmationModal = () => wrapper.findComponent(GlModal);
+
+ const itShowsConfirmationModal = (confirmText) => {
+ it('shows confirmation modal', async () => {
+ await wrapper.findByLabelText('Current password').setValue('foo bar');
+ await findDisableButton().trigger('click');
+
+ expect(findConfirmationModal().props('visible')).toBe(true);
+ expect(findConfirmationModal().html()).toContain(confirmText);
+ });
+ };
+
+ const itShowsValidationMessageIfCurrentPasswordFieldIsEmpty = (findButtonFunction) => {
+ it('shows validation message if `Current password` is empty', async () => {
+ await findButtonFunction().trigger('click');
+
+ expect(wrapper.findByText(i18n.currentPasswordInvalidFeedback).exists()).toBe(true);
+ });
+ };
beforeEach(() => {
createComponent();
});
- describe('Current password field', () => {
- it('renders the current password field', () => {
- expect(queryByLabelText(i18n.currentPassword).tagName).toEqual('INPUT');
+ describe('`Current password` field', () => {
+ describe('when required', () => {
+ it('renders the current password field', () => {
+ expect(wrapper.findByLabelText(i18n.currentPassword).exists()).toBe(true);
+ });
});
- });
- describe('when current password is not required', () => {
- beforeEach(() => {
- createComponent({
- currentPasswordRequired: false,
+ describe('when not required', () => {
+ beforeEach(() => {
+ createComponent({
+ currentPasswordRequired: false,
+ });
});
- });
- it('does not render the current password field', () => {
- expect(queryByLabelText(i18n.currentPassword)).toBe(null);
+ it('does not render the current password field', () => {
+ expect(wrapper.findByLabelText(i18n.currentPassword).exists()).toBe(false);
+ });
});
});
describe('Disable button', () => {
it('renders the component with correct attributes', () => {
expect(findDisableButton().exists()).toBe(true);
- expect(findDisableButton().attributes()).toMatchObject({
- 'data-confirm': i18n.confirm,
- 'data-form-action': defaultProvide.profileTwoFactorAuthPath,
- 'data-form-method': defaultProvide.profileTwoFactorAuthMethod,
- });
});
- it('has the right confirm text', () => {
- expect(findDisableButton().attributes('data-confirm')).toBe(i18n.confirm);
- });
+ describe('when clicked', () => {
+ itShowsValidationMessageIfCurrentPasswordFieldIsEmpty(findDisableButton);
- describe('when webauthnEnabled', () => {
- beforeEach(() => {
- createComponent({
- webauthnEnabled: true,
+ itShowsConfirmationModal(i18n.confirm);
+
+ describe('when webauthnEnabled', () => {
+ beforeEach(() => {
+ createComponent({
+ webauthnEnabled: true,
+ });
});
- });
- it('has the right confirm text', () => {
- expect(findDisableButton().attributes('data-confirm')).toBe(i18n.confirmWebAuthn);
+ itShowsConfirmationModal(i18n.confirmWebAuthn);
});
- });
- it('modifies the form action and method when submitted through the button', async () => {
- const form = findForm();
- const disableButton = findDisableButton().element;
- const methodInput = findMethodInput();
+ it('modifies the form action and method when submitted through the button', async () => {
+ const form = findForm();
+ const methodInput = findMethodInput();
+ const submitSpy = jest.spyOn(form.element, 'submit');
+
+ await wrapper.findByLabelText('Current password').setValue('foo bar');
+ await findDisableButton().trigger('click');
+
+ expect(form.attributes('action')).toBe(defaultProvide.profileTwoFactorAuthPath);
+ expect(methodInput.attributes('value')).toBe(defaultProvide.profileTwoFactorAuthMethod);
- await form.vm.$emit('submit', { submitter: disableButton });
+ findConfirmationModal().vm.$emit('primary');
- expect(form.attributes('action')).toBe(defaultProvide.profileTwoFactorAuthPath);
- expect(methodInput.attributes('value')).toBe(defaultProvide.profileTwoFactorAuthMethod);
+ expect(submitSpy).toHaveBeenCalled();
+ });
});
});
describe('Regenerate recovery codes button', () => {
it('renders the button', () => {
- expect(queryByText(i18n.regenerateRecoveryCodes)).toEqual(expect.any(HTMLElement));
+ expect(findRegenerateCodesButton().exists()).toBe(true);
});
- it('modifies the form action and method when submitted through the button', async () => {
- const form = findForm();
- const regenerateCodesButton = findRegenerateCodesButton().element;
- const methodInput = findMethodInput();
+ describe('when clicked', () => {
+ itShowsValidationMessageIfCurrentPasswordFieldIsEmpty(findRegenerateCodesButton);
+
+ it('modifies the form action and method when submitted through the button', async () => {
+ const form = findForm();
+ const methodInput = findMethodInput();
+ const submitSpy = jest.spyOn(form.element, 'submit');
- await form.vm.$emit('submit', { submitter: regenerateCodesButton });
+ await wrapper.findByLabelText('Current password').setValue('foo bar');
+ await findRegenerateCodesButton().trigger('click');
- expect(form.attributes('action')).toBe(defaultProvide.codesProfileTwoFactorAuthPath);
- expect(methodInput.attributes('value')).toBe(defaultProvide.codesProfileTwoFactorAuthMethod);
+ expect(form.attributes('action')).toBe(defaultProvide.codesProfileTwoFactorAuthPath);
+ expect(methodInput.attributes('value')).toBe(
+ defaultProvide.codesProfileTwoFactorAuthMethod,
+ );
+ expect(submitSpy).toHaveBeenCalled();
+ });
});
});
});
diff --git a/spec/frontend/batch_comments/components/preview_dropdown_spec.js b/spec/frontend/batch_comments/components/preview_dropdown_spec.js
index 41be04d0b7e..5327879f003 100644
--- a/spec/frontend/batch_comments/components/preview_dropdown_spec.js
+++ b/spec/frontend/batch_comments/components/preview_dropdown_spec.js
@@ -7,7 +7,7 @@ Vue.use(Vuex);
let wrapper;
-const toggleActiveFileByHash = jest.fn();
+const setCurrentFileHash = jest.fn();
const scrollToDraft = jest.fn();
function factory({ viewDiffsFileByFile = false, draftsCount = 1, sortedDrafts = [] } = {}) {
@@ -16,7 +16,7 @@ function factory({ viewDiffsFileByFile = false, draftsCount = 1, sortedDrafts =
diffs: {
namespaced: true,
actions: {
- toggleActiveFileByHash,
+ setCurrentFileHash,
},
state: {
viewDiffsFileByFile,
@@ -51,7 +51,7 @@ describe('Batch comments preview dropdown', () => {
await Vue.nextTick();
- expect(toggleActiveFileByHash).toHaveBeenCalledWith(expect.anything(), 'hash');
+ expect(setCurrentFileHash).toHaveBeenCalledWith(expect.anything(), 'hash');
expect(scrollToDraft).toHaveBeenCalledWith(expect.anything(), { id: 1, file_hash: 'hash' });
});
diff --git a/spec/frontend/behaviors/gl_emoji_spec.js b/spec/frontend/behaviors/gl_emoji_spec.js
index 286ed269421..d23a0a84997 100644
--- a/spec/frontend/behaviors/gl_emoji_spec.js
+++ b/spec/frontend/behaviors/gl_emoji_spec.js
@@ -56,13 +56,13 @@ describe('gl_emoji', () => {
'bomb emoji just with name attribute',
'<gl-emoji data-name="bomb"></gl-emoji>',
'<gl-emoji data-name="bomb" data-unicode-version="6.0" title="bomb">💣</gl-emoji>',
- '<gl-emoji data-name="bomb" data-unicode-version="6.0" title="bomb"><img class="emoji" title=":bomb:" alt=":bomb:" src="/-/emojis/1/bomb.png" width="20" height="20" align="absmiddle"></gl-emoji>',
+ `<gl-emoji data-name="bomb" data-unicode-version="6.0" title="bomb"><img class="emoji" title=":bomb:" alt=":bomb:" src="/-/emojis/${EMOJI_VERSION}/bomb.png" width="20" height="20" align="absmiddle"></gl-emoji>`,
],
[
'bomb emoji with name attribute and unicode version',
'<gl-emoji data-name="bomb" data-unicode-version="6.0">💣</gl-emoji>',
'<gl-emoji data-name="bomb" data-unicode-version="6.0">💣</gl-emoji>',
- '<gl-emoji data-name="bomb" data-unicode-version="6.0"><img class="emoji" title=":bomb:" alt=":bomb:" src="/-/emojis/1/bomb.png" width="20" height="20" align="absmiddle"></gl-emoji>',
+ `<gl-emoji data-name="bomb" data-unicode-version="6.0"><img class="emoji" title=":bomb:" alt=":bomb:" src="/-/emojis/${EMOJI_VERSION}/bomb.png" width="20" height="20" align="absmiddle"></gl-emoji>`,
],
[
'bomb emoji with sprite fallback',
@@ -80,7 +80,7 @@ describe('gl_emoji', () => {
'invalid emoji',
'<gl-emoji data-name="invalid_emoji"></gl-emoji>',
'<gl-emoji data-name="grey_question" data-unicode-version="6.0" title="white question mark ornament">❔</gl-emoji>',
- '<gl-emoji data-name="grey_question" data-unicode-version="6.0" title="white question mark ornament"><img class="emoji" title=":grey_question:" alt=":grey_question:" src="/-/emojis/1/grey_question.png" width="20" height="20" align="absmiddle"></gl-emoji>',
+ `<gl-emoji data-name="grey_question" data-unicode-version="6.0" title="white question mark ornament"><img class="emoji" title=":grey_question:" alt=":grey_question:" src="/-/emojis/${EMOJI_VERSION}/grey_question.png" width="20" height="20" align="absmiddle"></gl-emoji>`,
],
])('%s', (name, markup, withEmojiSupport, withoutEmojiSupport) => {
it(`renders correctly with emoji support`, async () => {
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 31fb6addcac..db9684239a1 100644
--- a/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap
+++ b/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap
@@ -4,9 +4,17 @@ exports[`Blob Header Default Actions rendering matches the snapshot 1`] = `
<div
class="js-file-title file-title-flex-parent"
>
- <blob-filepath-stub
- blob="[object Object]"
- />
+ <div
+ class="gl-display-flex"
+ >
+ <table-of-contents-stub
+ class="gl-pr-2"
+ />
+
+ <blob-filepath-stub
+ blob="[object Object]"
+ />
+ </div>
<div
class="gl-display-none gl-sm-display-flex"
diff --git a/spec/frontend/blob/components/blob_header_spec.js b/spec/frontend/blob/components/blob_header_spec.js
index f841785be42..bd81b1594bf 100644
--- a/spec/frontend/blob/components/blob_header_spec.js
+++ b/spec/frontend/blob/components/blob_header_spec.js
@@ -3,6 +3,7 @@ import BlobHeader from '~/blob/components/blob_header.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 TableContents from '~/blob/components/table_contents.vue';
import { Blob } from './mock_data';
@@ -43,6 +44,7 @@ describe('Blob Header Default Actions', () => {
it('renders all components', () => {
createComponent();
+ expect(wrapper.find(TableContents).exists()).toBe(true);
expect(wrapper.find(ViewerSwitcher).exists()).toBe(true);
expect(findDefaultActions().exists()).toBe(true);
expect(wrapper.find(BlobFilepath).exists()).toBe(true);
diff --git a/spec/frontend/blob/components/table_contents_spec.js b/spec/frontend/blob/components/table_contents_spec.js
index 09633dc5d5d..ade35d39b4f 100644
--- a/spec/frontend/blob/components/table_contents_spec.js
+++ b/spec/frontend/blob/components/table_contents_spec.js
@@ -32,10 +32,30 @@ describe('Markdown table of contents component', () => {
});
describe('not loaded', () => {
+ const findDropdownItem = () => wrapper.findComponent(GlDropdownItem);
+
it('does not populate dropdown', () => {
createComponent();
- expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(false);
+ expect(findDropdownItem().exists()).toBe(false);
+ });
+
+ it('does not show dropdown when loading blob content', async () => {
+ createComponent();
+
+ await setLoaded(false);
+
+ expect(findDropdownItem().exists()).toBe(false);
+ });
+
+ it('does not show dropdown when viewing non-rich content', async () => {
+ createComponent();
+
+ document.querySelector('.blob-viewer').setAttribute('data-type', 'simple');
+
+ await setLoaded(true);
+
+ expect(findDropdownItem().exists()).toBe(false);
});
});
diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js
index 25ec568e48d..5742dfdc5d2 100644
--- a/spec/frontend/boards/components/board_card_spec.js
+++ b/spec/frontend/boards/components/board_card_spec.js
@@ -64,12 +64,12 @@ describe('Board card', () => {
};
const selectCard = async () => {
- wrapper.trigger('mouseup');
+ wrapper.trigger('click');
await wrapper.vm.$nextTick();
};
const multiSelectCard = async () => {
- wrapper.trigger('mouseup', { ctrlKey: true });
+ wrapper.trigger('click', { ctrlKey: true });
await wrapper.vm.$nextTick();
};
diff --git a/spec/frontend/boards/components/board_filtered_search_spec.js b/spec/frontend/boards/components/board_filtered_search_spec.js
index dc93890f27a..b858d6e95a0 100644
--- a/spec/frontend/boards/components/board_filtered_search_spec.js
+++ b/spec/frontend/boards/components/board_filtered_search_spec.js
@@ -7,6 +7,7 @@ import { __ } from '~/locale';
import FilteredSearchBarRoot from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
+import { createStore } from '~/boards/stores';
Vue.use(Vuex);
@@ -42,17 +43,13 @@ describe('BoardFilteredSearch', () => {
},
];
- const createComponent = ({ initialFilterParams = {} } = {}) => {
- store = new Vuex.Store({
- actions: {
- performSearch: jest.fn(),
- },
- });
-
+ const createComponent = ({ initialFilterParams = {}, props = {} } = {}) => {
+ store = createStore();
wrapper = shallowMount(BoardFilteredSearch, {
provide: { initialFilterParams, fullPath: '' },
store,
propsData: {
+ ...props,
tokens,
},
});
@@ -68,11 +65,7 @@ describe('BoardFilteredSearch', () => {
beforeEach(() => {
createComponent();
- jest.spyOn(store, 'dispatch');
- });
-
- it('renders FilteredSearch', () => {
- expect(findFilteredSearch().exists()).toBe(true);
+ jest.spyOn(store, 'dispatch').mockImplementation();
});
it('passes the correct tokens to FilteredSearch', () => {
@@ -99,6 +92,22 @@ describe('BoardFilteredSearch', () => {
});
});
+ describe('when eeFilters is not empty', () => {
+ it('passes the correct initialFilterValue to FitleredSearchBarRoot', () => {
+ createComponent({ props: { eeFilters: { labelName: ['label'] } } });
+
+ expect(findFilteredSearch().props('initialFilterValue')).toEqual([
+ { type: 'label_name', value: { data: 'label', operator: '=' } },
+ ]);
+ });
+ });
+
+ it('renders FilteredSearch', () => {
+ createComponent();
+
+ expect(findFilteredSearch().exists()).toBe(true);
+ });
+
describe('when searching', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js
index 52f1907654a..692fd3ec555 100644
--- a/spec/frontend/boards/components/board_form_spec.js
+++ b/spec/frontend/boards/components/board_form_spec.js
@@ -1,7 +1,6 @@
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import setWindowLocation from 'helpers/set_window_location_helper';
-import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import BoardForm from '~/boards/components/board_form.vue';
@@ -18,21 +17,18 @@ jest.mock('~/lib/utils/url_utility', () => ({
}));
const currentBoard = {
- id: 1,
+ id: 'gid://gitlab/Board/1',
name: 'test',
labels: [],
- milestone_id: undefined,
+ milestone: {},
assignee: {},
- assignee_id: undefined,
weight: null,
- hide_backlog_list: false,
- hide_closed_list: false,
+ hideBacklogList: false,
+ hideClosedList: false,
};
const defaultProps = {
canAdminBoard: false,
- labelsPath: `${TEST_HOST}/labels/path`,
- labelsWebUrl: `${TEST_HOST}/-/labels`,
currentBoard,
currentPage: '',
};
@@ -252,7 +248,7 @@ describe('BoardForm', () => {
mutation: updateBoardMutation,
variables: {
input: expect.objectContaining({
- id: `gid://gitlab/Board/${currentBoard.id}`,
+ id: currentBoard.id,
}),
},
});
@@ -278,7 +274,7 @@ describe('BoardForm', () => {
mutation: updateBoardMutation,
variables: {
input: expect.objectContaining({
- id: `gid://gitlab/Board/${currentBoard.id}`,
+ id: currentBoard.id,
}),
},
});
@@ -326,7 +322,7 @@ describe('BoardForm', () => {
expect(mutate).toHaveBeenCalledWith({
mutation: destroyBoardMutation,
variables: {
- id: 'gid://gitlab/Board/1',
+ id: currentBoard.id,
},
});
diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js
index bf317b51e83..c841c17a029 100644
--- a/spec/frontend/boards/components/boards_selector_spec.js
+++ b/spec/frontend/boards/components/boards_selector_spec.js
@@ -1,13 +1,22 @@
import { GlDropdown, GlLoadingIcon, GlDropdownSectionHeader } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import Vuex from 'vuex';
import { TEST_HOST } from 'spec/test_constants';
import BoardsSelector from '~/boards/components/boards_selector.vue';
+import groupBoardQuery from '~/boards/graphql/group_board.query.graphql';
+import projectBoardQuery from '~/boards/graphql/project_board.query.graphql';
+import defaultStore from '~/boards/stores';
import axios from '~/lib/utils/axios_utils';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { mockGroupBoardResponse, mockProjectBoardResponse } from '../mock_data';
const throttleDuration = 1;
+Vue.use(VueApollo);
+
function boardGenerator(n) {
return new Array(n).fill().map((board, index) => {
const id = `${index}`;
@@ -25,9 +34,27 @@ describe('BoardsSelector', () => {
let allBoardsResponse;
let recentBoardsResponse;
let mock;
+ let fakeApollo;
+ let store;
const boards = boardGenerator(20);
const recentBoards = boardGenerator(5);
+ const createStore = ({ isGroupBoard = false, isProjectBoard = false } = {}) => {
+ store = new Vuex.Store({
+ ...defaultStore,
+ actions: {
+ setError: jest.fn(),
+ },
+ getters: {
+ isGroupBoard: () => isGroupBoard,
+ isProjectBoard: () => isProjectBoard,
+ },
+ state: {
+ boardType: isGroupBoard ? 'group' : 'project',
+ },
+ });
+ };
+
const fillSearchBox = (filterTerm) => {
const searchBox = wrapper.find({ ref: 'searchBox' });
const searchBoxInput = searchBox.find('input');
@@ -40,52 +67,27 @@ describe('BoardsSelector', () => {
const getLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findDropdown = () => wrapper.find(GlDropdown);
- beforeEach(() => {
- mock = new MockAdapter(axios);
- const $apollo = {
- queries: {
- boards: {
- loading: false,
- },
- },
- };
+ const projectBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockProjectBoardResponse);
+ const groupBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockGroupBoardResponse);
- allBoardsResponse = Promise.resolve({
- data: {
- group: {
- boards: {
- edges: boards.map((board) => ({ node: board })),
- },
- },
- },
- });
- recentBoardsResponse = Promise.resolve({
- data: recentBoards,
- });
+ const createComponent = () => {
+ fakeApollo = createMockApollo([
+ [projectBoardQuery, projectBoardQueryHandlerSuccess],
+ [groupBoardQuery, groupBoardQueryHandlerSuccess],
+ ]);
wrapper = mount(BoardsSelector, {
+ store,
+ apolloProvider: fakeApollo,
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,
provide: {
fullPath: '',
@@ -98,12 +100,7 @@ describe('BoardsSelector', () => {
[options.loadingKey]: true,
});
});
-
- 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');
- });
+ };
afterEach(() => {
wrapper.destroy();
@@ -111,104 +108,158 @@ describe('BoardsSelector', () => {
mock.restore();
});
- describe('loading', () => {
- // we are testing loading state, so don't resolve responses until after the tests
- afterEach(() => {
- return Promise.all([allBoardsResponse, recentBoardsResponse]).then(() => nextTick());
- });
+ describe('fetching all boards', () => {
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
- it('shows loading spinner', () => {
- expect(getDropdownHeaders()).toHaveLength(0);
- expect(getDropdownItems()).toHaveLength(0);
- expect(getLoadingIcon().exists()).toBe(true);
+ allBoardsResponse = Promise.resolve({
+ data: {
+ group: {
+ boards: {
+ edges: boards.map((board) => ({ node: board })),
+ },
+ },
+ },
+ });
+ recentBoardsResponse = Promise.resolve({
+ data: recentBoards,
+ });
+
+ createStore();
+ createComponent();
+
+ mock.onGet(`${TEST_HOST}/recent`).replyOnce(200, recentBoards);
});
- });
- describe('loaded', () => {
- beforeEach(async () => {
- await wrapper.setData({
- loadingBoards: false,
+ describe('loading', () => {
+ beforeEach(async () => {
+ // Wait for current board to be loaded
+ await nextTick();
+
+ // Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
+ findDropdown().vm.$emit('show');
+ });
+
+ // we are testing loading state, so don't resolve responses until after the tests
+ afterEach(async () => {
+ await Promise.all([allBoardsResponse, recentBoardsResponse]);
+ await nextTick();
});
- return Promise.all([allBoardsResponse, recentBoardsResponse]).then(() => nextTick());
- });
- it('hides loading spinner', async () => {
- await wrapper.vm.$nextTick();
- expect(getLoadingIcon().exists()).toBe(false);
+ it('shows loading spinner', () => {
+ expect(getDropdownHeaders()).toHaveLength(0);
+ expect(getDropdownItems()).toHaveLength(0);
+ expect(getLoadingIcon().exists()).toBe(true);
+ });
});
- describe('filtering', () => {
- beforeEach(() => {
- wrapper.setData({
- boards,
- });
+ describe('loaded', () => {
+ beforeEach(async () => {
+ // Wait for current board to be loaded
+ await nextTick();
+
+ // Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
+ findDropdown().vm.$emit('show');
- return nextTick();
+ await wrapper.setData({
+ loadingBoards: false,
+ loadingRecentBoards: false,
+ });
+ await Promise.all([allBoardsResponse, recentBoardsResponse]);
+ await nextTick();
});
- it('shows all boards without filtering', () => {
- expect(getDropdownItems()).toHaveLength(boards.length + recentBoards.length);
+ it('hides loading spinner', async () => {
+ await nextTick();
+ expect(getLoadingIcon().exists()).toBe(false);
});
- it('shows only matching boards when filtering', () => {
- const filterTerm = 'board1';
- const expectedCount = boards.filter((board) => board.name.includes(filterTerm)).length;
+ describe('filtering', () => {
+ beforeEach(async () => {
+ wrapper.setData({
+ boards,
+ });
+
+ await nextTick();
+ });
- fillSearchBox(filterTerm);
+ it('shows all boards without filtering', () => {
+ expect(getDropdownItems()).toHaveLength(boards.length + recentBoards.length);
+ });
- return nextTick().then(() => {
+ it('shows only matching boards when filtering', async () => {
+ const filterTerm = 'board1';
+ const expectedCount = boards.filter((board) => board.name.includes(filterTerm)).length;
+
+ fillSearchBox(filterTerm);
+
+ await nextTick();
expect(getDropdownItems()).toHaveLength(expectedCount);
});
- });
- it('shows message if there are no matching boards', () => {
- fillSearchBox('does not exist');
+ it('shows message if there are no matching boards', async () => {
+ fillSearchBox('does not exist');
- return nextTick().then(() => {
+ await nextTick();
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,
- });
+ describe('recent boards section', () => {
+ it('shows only when boards are greater than 10', async () => {
+ wrapper.setData({
+ boards,
+ });
- return nextTick().then(() => {
+ await nextTick();
expect(getDropdownHeaders()).toHaveLength(2);
});
- });
- it('does not show when boards are less than 10', () => {
- wrapper.setData({
- boards: boards.slice(0, 5),
- });
+ it('does not show when boards are less than 10', async () => {
+ wrapper.setData({
+ boards: boards.slice(0, 5),
+ });
- return nextTick().then(() => {
+ await nextTick();
expect(getDropdownHeaders()).toHaveLength(0);
});
- });
- it('does not show when recentBoards api returns empty array', () => {
- wrapper.setData({
- recentBoards: [],
- });
+ it('does not show when recentBoards api returns empty array', async () => {
+ wrapper.setData({
+ recentBoards: [],
+ });
- return nextTick().then(() => {
+ await nextTick();
expect(getDropdownHeaders()).toHaveLength(0);
});
- });
- it('does not show when search is active', () => {
- fillSearchBox('Random string');
+ it('does not show when search is active', async () => {
+ fillSearchBox('Random string');
- return nextTick().then(() => {
+ await nextTick();
expect(getDropdownHeaders()).toHaveLength(0);
});
});
});
});
+
+ describe('fetching current board', () => {
+ it.each`
+ boardType | queryHandler | notCalledHandler
+ ${'group'} | ${groupBoardQueryHandlerSuccess} | ${projectBoardQueryHandlerSuccess}
+ ${'project'} | ${projectBoardQueryHandlerSuccess} | ${groupBoardQueryHandlerSuccess}
+ `('fetches $boardType board', async ({ boardType, queryHandler, notCalledHandler }) => {
+ createStore({
+ isProjectBoard: boardType === 'project',
+ isGroupBoard: boardType === 'group',
+ });
+ createComponent();
+
+ await nextTick();
+
+ expect(queryHandler).toHaveBeenCalled();
+ expect(notCalledHandler).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/boards/components/issue_board_filtered_search_spec.js b/spec/frontend/boards/components/issue_board_filtered_search_spec.js
index b6de46f8db8..45c5c87d800 100644
--- a/spec/frontend/boards/components/issue_board_filtered_search_spec.js
+++ b/spec/frontend/boards/components/issue_board_filtered_search_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import BoardFilteredSearch from '~/boards/components/board_filtered_search.vue';
+import BoardFilteredSearch from 'ee_else_ce/boards/components/board_filtered_search.vue';
import IssueBoardFilteredSpec from '~/boards/components/issue_board_filtered_search.vue';
import issueBoardFilters from '~/boards/issue_board_filters';
import { mockTokens } from '../mock_data';
@@ -9,39 +9,60 @@ jest.mock('~/boards/issue_board_filters');
describe('IssueBoardFilter', () => {
let wrapper;
- const createComponent = () => {
+ const findBoardsFilteredSearch = () => wrapper.findComponent(BoardFilteredSearch);
+
+ const createComponent = ({ isSignedIn = false } = {}) => {
wrapper = shallowMount(IssueBoardFilteredSpec, {
- props: { fullPath: '', boardType: '' },
+ propsData: { fullPath: 'gitlab-org', boardType: 'group' },
+ provide: {
+ isSignedIn,
+ },
});
};
+ let fetchAuthorsSpy;
+ let fetchLabelsSpy;
+ beforeEach(() => {
+ fetchAuthorsSpy = jest.fn();
+ fetchLabelsSpy = jest.fn();
+
+ issueBoardFilters.mockReturnValue({
+ fetchAuthors: fetchAuthorsSpy,
+ fetchLabels: fetchLabelsSpy,
+ });
+ });
+
afterEach(() => {
wrapper.destroy();
});
describe('default', () => {
- let fetchAuthorsSpy;
- let fetchLabelsSpy;
beforeEach(() => {
- fetchAuthorsSpy = jest.fn();
- fetchLabelsSpy = jest.fn();
-
- issueBoardFilters.mockReturnValue({
- fetchAuthors: fetchAuthorsSpy,
- fetchLabels: fetchLabelsSpy,
- });
-
createComponent();
});
it('finds BoardFilteredSearch', () => {
- expect(wrapper.find(BoardFilteredSearch).exists()).toBe(true);
+ expect(findBoardsFilteredSearch().exists()).toBe(true);
});
- it('passes the correct tokens to BoardFilteredSearch', () => {
- const tokens = mockTokens(fetchLabelsSpy, fetchAuthorsSpy, wrapper.vm.fetchMilestones);
+ it.each`
+ isSignedIn
+ ${true}
+ ${false}
+ `(
+ 'passes the correct tokens to BoardFilteredSearch when user sign in is $isSignedIn',
+ ({ isSignedIn }) => {
+ createComponent({ isSignedIn });
- expect(wrapper.find(BoardFilteredSearch).props('tokens')).toEqual(tokens);
- });
+ const tokens = mockTokens(
+ fetchLabelsSpy,
+ fetchAuthorsSpy,
+ wrapper.vm.fetchMilestones,
+ isSignedIn,
+ );
+
+ expect(findBoardsFilteredSearch().props('tokens')).toEqual(tokens);
+ },
+ );
});
});
diff --git a/spec/frontend/boards/components/new_board_button_spec.js b/spec/frontend/boards/components/new_board_button_spec.js
new file mode 100644
index 00000000000..075fe225ec2
--- /dev/null
+++ b/spec/frontend/boards/components/new_board_button_spec.js
@@ -0,0 +1,75 @@
+import { mount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
+import NewBoardButton from '~/boards/components/new_board_button.vue';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { stubExperiments } from 'helpers/experimentation_helper';
+import eventHub from '~/boards/eventhub';
+
+const FEATURE = 'prominent_create_board_btn';
+
+describe('NewBoardButton', () => {
+ let wrapper;
+
+ const createComponent = (args = {}) =>
+ extendedWrapper(
+ mount(NewBoardButton, {
+ provide: {
+ canAdminBoard: true,
+ multipleIssueBoardsAvailable: true,
+ ...args,
+ },
+ }),
+ );
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ }
+ });
+
+ describe('control variant', () => {
+ beforeAll(() => {
+ stubExperiments({ [FEATURE]: 'control' });
+ });
+
+ it('renders nothing', () => {
+ wrapper = createComponent();
+
+ expect(wrapper.text()).toBe('');
+ });
+ });
+
+ describe('candidate variant', () => {
+ beforeAll(() => {
+ stubExperiments({ [FEATURE]: 'candidate' });
+ });
+
+ it('renders New board button when `candidate` variant', () => {
+ wrapper = createComponent();
+
+ expect(wrapper.text()).toBe('New board');
+ });
+
+ it('renders nothing when `canAdminBoard` is `false`', () => {
+ wrapper = createComponent({ canAdminBoard: false });
+
+ expect(wrapper.find(GlButton).exists()).toBe(false);
+ });
+
+ it('renders nothing when `multipleIssueBoardsAvailable` is `false`', () => {
+ wrapper = createComponent({ multipleIssueBoardsAvailable: false });
+
+ expect(wrapper.find(GlButton).exists()).toBe(false);
+ });
+
+ it('emits `showBoardModal` when button is clicked', () => {
+ jest.spyOn(eventHub, '$emit').mockImplementation();
+
+ wrapper = createComponent();
+
+ wrapper.find(GlButton).vm.$emit('click', { preventDefault: () => {} });
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('showBoardModal', 'new');
+ });
+ });
+});
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 60474767f2d..fb9d823107e 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
@@ -105,6 +105,7 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => {
describe('when labels are updated over existing labels', () => {
const testLabelsPayload = [
{ id: 5, set: true },
+ { id: 6, set: false },
{ id: 7, set: true },
];
const expectedLabels = [{ id: 5 }, { id: 7 }];
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 8847f626c1f..6e1b528babc 100644
--- a/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js
@@ -14,8 +14,8 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () =
let store;
const findNotificationHeader = () => wrapper.find("[data-testid='notification-header-text']");
- const findToggle = () => wrapper.find(GlToggle);
- const findGlLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const findToggle = () => wrapper.findComponent(GlToggle);
+ const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const createComponent = (activeBoardItem = { ...mockActiveIssue }) => {
store = createStore();
@@ -32,7 +32,6 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () =
afterEach(() => {
wrapper.destroy();
- wrapper = null;
store = null;
jest.clearAllMocks();
});
@@ -104,7 +103,7 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () =
expect(findGlLoadingIcon().exists()).toBe(false);
- findToggle().trigger('click');
+ findToggle().vm.$emit('change');
await wrapper.vm.$nextTick();
@@ -129,7 +128,7 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () =
expect(findGlLoadingIcon().exists()).toBe(false);
- findToggle().trigger('click');
+ findToggle().vm.$emit('change');
await wrapper.vm.$nextTick();
@@ -152,7 +151,7 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () =
});
jest.spyOn(wrapper.vm, 'setError').mockImplementation(() => {});
- findToggle().trigger('click');
+ findToggle().vm.$emit('change');
await wrapper.vm.$nextTick();
expect(wrapper.vm.setError).toHaveBeenCalled();
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index 6a4f344bbfb..8fcad99f8a7 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -4,6 +4,7 @@ import { ListType } from '~/boards/constants';
import { __ } from '~/locale';
import { DEFAULT_MILESTONES_GRAPHQL } from '~/vue_shared/components/filtered_search_bar/constants';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
+import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue';
@@ -12,6 +13,7 @@ export const boardObj = {
id: 1,
name: 'test',
milestone_id: null,
+ labels: [],
};
export const listObj = {
@@ -29,17 +31,27 @@ export const listObj = {
},
};
-export const listObjDuplicate = {
- id: listObj.id,
- position: 1,
- title: 'Test',
- list_type: 'label',
- weight: 3,
- label: {
- id: listObj.label.id,
- title: 'Test',
- color: '#ff0000',
- description: 'testing;',
+export const mockGroupBoardResponse = {
+ data: {
+ workspace: {
+ board: {
+ id: 'gid://gitlab/Board/1',
+ name: 'Development',
+ },
+ __typename: 'Group',
+ },
+ },
+};
+
+export const mockProjectBoardResponse = {
+ data: {
+ workspace: {
+ board: {
+ id: 'gid://gitlab/Board/2',
+ name: 'Development',
+ },
+ __typename: 'Project',
+ },
},
};
@@ -538,7 +550,16 @@ export const mockMoveData = {
...mockMoveIssueParams,
};
-export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones) => [
+export const mockEmojiToken = {
+ type: 'my_reaction_emoji',
+ icon: 'thumb-up',
+ title: 'My-Reaction',
+ unique: true,
+ token: EmojiToken,
+ fetchEmojis: expect.any(Function),
+};
+
+export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, hasEmoji) => [
{
icon: 'user',
title: __('Assignee'),
@@ -579,6 +600,7 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones) => [
symbol: '~',
fetchLabels,
},
+ ...(hasEmoji ? [mockEmojiToken] : []),
{
icon: 'clock',
title: __('Milestone'),
@@ -593,7 +615,6 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones) => [
icon: 'issues',
title: __('Type'),
type: 'types',
- operators: [{ value: '=', description: 'is' }],
token: GlFilteredSearchToken,
unique: true,
options: [
@@ -609,3 +630,43 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones) => [
unique: true,
},
];
+
+export const mockLabel1 = {
+ id: 'gid://gitlab/GroupLabel/121',
+ title: 'To Do',
+ color: '#F0AD4E',
+ textColor: '#FFFFFF',
+ description: null,
+};
+
+export const mockLabel2 = {
+ id: 'gid://gitlab/GroupLabel/122',
+ title: 'Doing',
+ color: '#F0AD4E',
+ textColor: '#FFFFFF',
+ description: null,
+};
+
+export const mockProjectLabelsResponse = {
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Project/1',
+ labels: {
+ nodes: [mockLabel1, mockLabel2],
+ },
+ __typename: 'Project',
+ },
+ },
+};
+
+export const mockGroupLabelsResponse = {
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Group/1',
+ labels: {
+ nodes: [mockLabel1, mockLabel2],
+ },
+ __typename: 'Group',
+ },
+ },
+};
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index 0b90912a584..e245325b956 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -27,6 +27,7 @@ import issueCreateMutation from '~/boards/graphql/issue_create.mutation.graphql'
import actions from '~/boards/stores/actions';
import * as types from '~/boards/stores/mutation_types';
import mutations from '~/boards/stores/mutations';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import {
mockLists,
@@ -1572,12 +1573,13 @@ describe('setActiveIssueLabels', () => {
const getters = { activeBoardItem: mockIssue };
const testLabelIds = labels.map((label) => label.id);
const input = {
- addLabelIds: testLabelIds,
+ labelIds: testLabelIds,
removeLabelIds: [],
projectPath: 'h/b',
+ labels,
};
- it('should assign labels on success, and sets loading state for labels', (done) => {
+ it('should assign labels on success', (done) => {
jest
.spyOn(gqlClient, 'mutate')
.mockResolvedValue({ data: { updateIssue: { issue: { labels: { nodes: labels } } } } });
@@ -1594,14 +1596,6 @@ describe('setActiveIssueLabels', () => {
{ ...state, ...getters },
[
{
- type: types.SET_LABELS_LOADING,
- payload: true,
- },
- {
- type: types.SET_LABELS_LOADING,
- payload: false,
- },
- {
type: types.UPDATE_BOARD_ITEM_BY_ID,
payload,
},
@@ -1618,6 +1612,64 @@ describe('setActiveIssueLabels', () => {
await expect(actions.setActiveIssueLabels({ getters }, input)).rejects.toThrow(Error);
});
+
+ describe('labels_widget FF on', () => {
+ beforeEach(() => {
+ window.gon = {
+ features: { labelsWidget: true },
+ };
+
+ getters.activeBoardItem = { ...mockIssue, labels };
+ });
+
+ afterEach(() => {
+ window.gon = {
+ features: {},
+ };
+ });
+
+ it('should assign labels', () => {
+ const payload = {
+ itemId: getters.activeBoardItem.id,
+ prop: 'labels',
+ value: labels,
+ };
+
+ testAction(
+ actions.setActiveIssueLabels,
+ input,
+ { ...state, ...getters },
+ [
+ {
+ type: types.UPDATE_BOARD_ITEM_BY_ID,
+ payload,
+ },
+ ],
+ [],
+ );
+ });
+
+ it('should remove label', () => {
+ const payload = {
+ itemId: getters.activeBoardItem.id,
+ prop: 'labels',
+ value: [labels[1]],
+ };
+
+ testAction(
+ actions.setActiveIssueLabels,
+ { ...input, removeLabelIds: [getIdFromGraphQLId(labels[0].id)] },
+ { ...state, ...getters },
+ [
+ {
+ type: types.UPDATE_BOARD_ITEM_BY_ID,
+ payload,
+ },
+ ],
+ [],
+ );
+ });
+ });
});
describe('setActiveItemSubscribed', () => {
diff --git a/spec/frontend/chronic_duration_spec.js b/spec/frontend/chronic_duration_spec.js
new file mode 100644
index 00000000000..32652e13dfc
--- /dev/null
+++ b/spec/frontend/chronic_duration_spec.js
@@ -0,0 +1,354 @@
+/*
+ * NOTE:
+ * Changes to this file should be kept in sync with
+ * https://gitlab.com/gitlab-org/gitlab-chronic-duration/-/blob/master/spec/lib/chronic_duration_spec.rb.
+ */
+
+/*
+ * This code is based on code from
+ * https://gitlab.com/gitlab-org/gitlab-chronic-duration and is
+ * distributed under the following license:
+ *
+ * MIT License
+ *
+ * Copyright (c) Henry Poydar
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+import {
+ parseChronicDuration,
+ outputChronicDuration,
+ DurationParseError,
+} from '~/chronic_duration';
+
+describe('parseChronicDuration', () => {
+ /*
+ * TODO The Ruby implementation of this algorithm uses the Numerizer module,
+ * which converts strings like "forty two" to "42", but there is no
+ * JavaScript equivalent of Numerizer. Skip it for now until Numerizer is
+ * ported to JavaScript.
+ */
+ const EXEMPLARS = {
+ '1:20': 60 + 20,
+ '1:20.51': 60 + 20.51,
+ '4:01:01': 4 * 3600 + 60 + 1,
+ '3 mins 4 sec': 3 * 60 + 4,
+ '3 Mins 4 Sec': 3 * 60 + 4,
+ // 'three mins four sec': 3 * 60 + 4,
+ '2 hrs 20 min': 2 * 3600 + 20 * 60,
+ '2h20min': 2 * 3600 + 20 * 60,
+ '6 mos 1 day': 6 * 30 * 24 * 3600 + 24 * 3600,
+ '1 year 6 mos 1 day': 1 * 31557600 + 6 * 30 * 24 * 3600 + 24 * 3600,
+ '2.5 hrs': 2.5 * 3600,
+ '47 yrs 6 mos and 4.5d': 47 * 31557600 + 6 * 30 * 24 * 3600 + 4.5 * 24 * 3600,
+ // 'two hours and twenty minutes': 2 * 3600 + 20 * 60,
+ // 'four hours and forty minutes': 4 * 3600 + 40 * 60,
+ // 'four hours, and fourty minutes': 4 * 3600 + 40 * 60,
+ '3 weeks and, 2 days': 3600 * 24 * 7 * 3 + 3600 * 24 * 2,
+ '3 weeks, plus 2 days': 3600 * 24 * 7 * 3 + 3600 * 24 * 2,
+ '3 weeks with 2 days': 3600 * 24 * 7 * 3 + 3600 * 24 * 2,
+ '1 month': 3600 * 24 * 30,
+ '2 months': 3600 * 24 * 30 * 2,
+ '18 months': 3600 * 24 * 30 * 18,
+ '1 year 6 months': 3600 * 24 * (365.25 + 6 * 30),
+ day: 3600 * 24,
+ 'minute 30s': 90,
+ };
+
+ describe("when string can't be parsed", () => {
+ it('returns null', () => {
+ expect(parseChronicDuration('gobblygoo')).toBeNull();
+ });
+
+ it('cannot parse zero', () => {
+ expect(parseChronicDuration('0')).toBeNull();
+ });
+
+ describe('when .raiseExceptions set to true', () => {
+ it('raises with DurationParseError', () => {
+ expect(() => parseChronicDuration('23 gobblygoos', { raiseExceptions: true })).toThrowError(
+ DurationParseError,
+ );
+ });
+
+ it('does not raise when string is empty', () => {
+ expect(parseChronicDuration('', { raiseExceptions: true })).toBeNull();
+ });
+ });
+ });
+
+ it('should return zero if the string parses as zero and the .keepZero option is true', () => {
+ expect(parseChronicDuration('0', { keepZero: true })).toBe(0);
+ });
+
+ it('should return a float if seconds are in decimals', () => {
+ expect(parseChronicDuration('12 mins 3.141 seconds')).toBeCloseTo(723.141, 4);
+ });
+
+ it('should return an integer unless the seconds are in decimals', () => {
+ expect(parseChronicDuration('12 mins 3 seconds')).toBe(723);
+ });
+
+ it('should be able to parse minutes by default', () => {
+ expect(parseChronicDuration('5', { defaultUnit: 'minutes' })).toBe(300);
+ });
+
+ Object.entries(EXEMPLARS).forEach(([k, v]) => {
+ it(`parses a duration like ${k}`, () => {
+ expect(parseChronicDuration(k)).toBe(v);
+ });
+ });
+
+ describe('with .hoursPerDay and .daysPerMonth params', () => {
+ it('uses provided .hoursPerDay', () => {
+ expect(parseChronicDuration('1d', { hoursPerDay: 24 })).toBe(24 * 60 * 60);
+ expect(parseChronicDuration('1d', { hoursPerDay: 8 })).toBe(8 * 60 * 60);
+ });
+
+ it('uses provided .daysPerMonth', () => {
+ expect(parseChronicDuration('1mo', { daysPerMonth: 30 })).toBe(30 * 24 * 60 * 60);
+ expect(parseChronicDuration('1mo', { daysPerMonth: 20 })).toBe(20 * 24 * 60 * 60);
+
+ expect(parseChronicDuration('1w', { daysPerMonth: 30 })).toBe(7 * 24 * 60 * 60);
+ expect(parseChronicDuration('1w', { daysPerMonth: 20 })).toBe(5 * 24 * 60 * 60);
+ });
+
+ it('uses provided both .hoursPerDay and .daysPerMonth', () => {
+ expect(parseChronicDuration('1mo', { daysPerMonth: 30, hoursPerDay: 24 })).toBe(
+ 30 * 24 * 60 * 60,
+ );
+ expect(parseChronicDuration('1mo', { daysPerMonth: 20, hoursPerDay: 8 })).toBe(
+ 20 * 8 * 60 * 60,
+ );
+
+ expect(parseChronicDuration('1w', { daysPerMonth: 30, hoursPerDay: 24 })).toBe(
+ 7 * 24 * 60 * 60,
+ );
+ expect(parseChronicDuration('1w', { daysPerMonth: 20, hoursPerDay: 8 })).toBe(
+ 5 * 8 * 60 * 60,
+ );
+ });
+ });
+});
+
+describe('outputChronicDuration', () => {
+ const EXEMPLARS = {
+ [60 + 20]: {
+ micro: '1m20s',
+ short: '1m 20s',
+ default: '1 min 20 secs',
+ long: '1 minute 20 seconds',
+ chrono: '1:20',
+ },
+ [60 + 20.51]: {
+ micro: '1m20.51s',
+ short: '1m 20.51s',
+ default: '1 min 20.51 secs',
+ long: '1 minute 20.51 seconds',
+ chrono: '1:20.51',
+ },
+ [60 + 20.51928]: {
+ micro: '1m20.51928s',
+ short: '1m 20.51928s',
+ default: '1 min 20.51928 secs',
+ long: '1 minute 20.51928 seconds',
+ chrono: '1:20.51928',
+ },
+ [4 * 3600 + 60 + 1]: {
+ micro: '4h1m1s',
+ short: '4h 1m 1s',
+ default: '4 hrs 1 min 1 sec',
+ long: '4 hours 1 minute 1 second',
+ chrono: '4:01:01',
+ },
+ [2 * 3600 + 20 * 60]: {
+ micro: '2h20m',
+ short: '2h 20m',
+ default: '2 hrs 20 mins',
+ long: '2 hours 20 minutes',
+ chrono: '2:20',
+ },
+ [2 * 3600 + 20 * 60]: {
+ micro: '2h20m',
+ short: '2h 20m',
+ default: '2 hrs 20 mins',
+ long: '2 hours 20 minutes',
+ chrono: '2:20:00',
+ },
+ [6 * 30 * 24 * 3600 + 24 * 3600]: {
+ micro: '6mo1d',
+ short: '6mo 1d',
+ default: '6 mos 1 day',
+ long: '6 months 1 day',
+ chrono: '6:01:00:00:00', // Yuck. FIXME
+ },
+ [365.25 * 24 * 3600 + 24 * 3600]: {
+ micro: '1y1d',
+ short: '1y 1d',
+ default: '1 yr 1 day',
+ long: '1 year 1 day',
+ chrono: '1:00:01:00:00:00',
+ },
+ [3 * 365.25 * 24 * 3600 + 24 * 3600]: {
+ micro: '3y1d',
+ short: '3y 1d',
+ default: '3 yrs 1 day',
+ long: '3 years 1 day',
+ chrono: '3:00:01:00:00:00',
+ },
+ [3600 * 24 * 30 * 18]: {
+ micro: '18mo',
+ short: '18mo',
+ default: '18 mos',
+ long: '18 months',
+ chrono: '18:00:00:00:00',
+ },
+ };
+
+ Object.entries(EXEMPLARS).forEach(([k, v]) => {
+ const kf = parseFloat(k);
+ Object.entries(v).forEach(([key, val]) => {
+ it(`properly outputs a duration of ${kf} seconds as ${val} using the ${key} format option`, () => {
+ expect(outputChronicDuration(kf, { format: key })).toBe(val);
+ });
+ });
+ });
+
+ const KEEP_ZERO_EXEMPLARS = {
+ true: {
+ micro: '0s',
+ short: '0s',
+ default: '0 secs',
+ long: '0 seconds',
+ chrono: '0',
+ },
+ '': {
+ micro: null,
+ short: null,
+ default: null,
+ long: null,
+ chrono: '0',
+ },
+ };
+
+ Object.entries(KEEP_ZERO_EXEMPLARS).forEach(([k, v]) => {
+ const kb = Boolean(k);
+ Object.entries(v).forEach(([key, val]) => {
+ it(`should properly output a duration of 0 seconds as ${val} using the ${key} format option, if the .keepZero option is ${kb}`, () => {
+ expect(outputChronicDuration(0, { format: key, keepZero: kb })).toBe(val);
+ });
+ });
+ });
+
+ it('returns weeks when needed', () => {
+ expect(outputChronicDuration(45 * 24 * 60 * 60, { weeks: true })).toMatch(/.*wk.*/);
+ });
+
+ it('returns hours and minutes only when .limitToHours option specified', () => {
+ expect(outputChronicDuration(395 * 24 * 60 * 60 + 15 * 60, { limitToHours: true })).toBe(
+ '9480 hrs 15 mins',
+ );
+ });
+
+ describe('with .hoursPerDay and .daysPerMonth params', () => {
+ it('uses provided .hoursPerDay', () => {
+ expect(outputChronicDuration(24 * 60 * 60, { hoursPerDay: 24 })).toBe('1 day');
+ expect(outputChronicDuration(24 * 60 * 60, { hoursPerDay: 8 })).toBe('3 days');
+ });
+
+ it('uses provided .daysPerMonth', () => {
+ expect(outputChronicDuration(7 * 24 * 60 * 60, { weeks: true, daysPerMonth: 30 })).toBe(
+ '1 wk',
+ );
+ expect(outputChronicDuration(7 * 24 * 60 * 60, { weeks: true, daysPerMonth: 20 })).toBe(
+ '1 wk 2 days',
+ );
+ });
+
+ it('uses provided both .hoursPerDay and .daysPerMonth', () => {
+ expect(
+ outputChronicDuration(7 * 24 * 60 * 60, { weeks: true, daysPerMonth: 30, hoursPerDay: 24 }),
+ ).toBe('1 wk');
+ expect(
+ outputChronicDuration(5 * 8 * 60 * 60, { weeks: true, daysPerMonth: 20, hoursPerDay: 8 }),
+ ).toBe('1 wk');
+ });
+
+ it('uses provided params alongside with .weeks when converting to months', () => {
+ expect(outputChronicDuration(30 * 24 * 60 * 60, { daysPerMonth: 30, hoursPerDay: 24 })).toBe(
+ '1 mo',
+ );
+ expect(
+ outputChronicDuration(30 * 24 * 60 * 60, {
+ daysPerMonth: 30,
+ hoursPerDay: 24,
+ weeks: true,
+ }),
+ ).toBe('1 mo 2 days');
+
+ expect(outputChronicDuration(20 * 8 * 60 * 60, { daysPerMonth: 20, hoursPerDay: 8 })).toBe(
+ '1 mo',
+ );
+ expect(
+ outputChronicDuration(20 * 8 * 60 * 60, { daysPerMonth: 20, hoursPerDay: 8, weeks: true }),
+ ).toBe('1 mo');
+ });
+ });
+
+ it('returns the specified number of units if provided', () => {
+ expect(outputChronicDuration(4 * 3600 + 60 + 1, { units: 2 })).toBe('4 hrs 1 min');
+ expect(
+ outputChronicDuration(6 * 30 * 24 * 3600 + 24 * 3600 + 3600 + 60 + 1, {
+ units: 3,
+ format: 'long',
+ }),
+ ).toBe('6 months 1 day 1 hour');
+ });
+
+ describe('when the format is not specified', () => {
+ it('uses the default format', () => {
+ expect(outputChronicDuration(2 * 3600 + 20 * 60)).toBe('2 hrs 20 mins');
+ });
+ });
+
+ Object.entries(EXEMPLARS).forEach(([seconds, formatSpec]) => {
+ const secondsF = parseFloat(seconds);
+ Object.keys(formatSpec).forEach((format) => {
+ it(`outputs a duration for ${seconds} that parses back to the same thing when using the ${format} format`, () => {
+ expect(parseChronicDuration(outputChronicDuration(secondsF, { format }))).toBe(secondsF);
+ });
+ });
+ });
+
+ it('uses user-specified joiner if provided', () => {
+ expect(outputChronicDuration(2 * 3600 + 20 * 60, { joiner: ', ' })).toBe('2 hrs, 20 mins');
+ });
+});
+
+describe('work week', () => {
+ it('should parse knowing the work week', () => {
+ const week = parseChronicDuration('5d', { hoursPerDay: 8, daysPerMonth: 20 });
+ expect(parseChronicDuration('40h', { hoursPerDay: 8, daysPerMonth: 20 })).toBe(week);
+ expect(parseChronicDuration('1w', { hoursPerDay: 8, daysPerMonth: 20 })).toBe(week);
+ });
+});
diff --git a/spec/frontend/clusters/agents/components/show_spec.js b/spec/frontend/clusters/agents/components/show_spec.js
index fd04ff8b3e7..c502e7d813e 100644
--- a/spec/frontend/clusters/agents/components/show_spec.js
+++ b/spec/frontend/clusters/agents/components/show_spec.js
@@ -1,6 +1,8 @@
import { GlAlert, GlKeysetPagination, GlLoadingIcon, GlSprintf, GlTab } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import ClusterAgentShow from '~/clusters/agents/components/show.vue';
import TokenTable from '~/clusters/agents/components/token_table.vue';
import getAgentQuery from '~/clusters/agents/graphql/queries/get_cluster_agent.query.graphql';
@@ -40,28 +42,34 @@ describe('ClusterAgentShow', () => {
queryResponse || jest.fn().mockResolvedValue({ data: { project: { clusterAgent } } });
const apolloProvider = createMockApollo([[getAgentQuery, agentQueryResponse]]);
- wrapper = shallowMount(ClusterAgentShow, {
- localVue,
- apolloProvider,
- propsData,
- stubs: { GlSprintf, TimeAgoTooltip, GlTab },
- });
+ wrapper = extendedWrapper(
+ shallowMount(ClusterAgentShow, {
+ localVue,
+ apolloProvider,
+ propsData,
+ stubs: { GlSprintf, TimeAgoTooltip, GlTab },
+ }),
+ );
};
- const createWrapperWithoutApollo = ({ clusterAgent, loading = false }) => {
+ const createWrapperWithoutApollo = ({ clusterAgent, loading = false, slots = {} }) => {
const $apollo = { queries: { clusterAgent: { loading } } };
- wrapper = shallowMount(ClusterAgentShow, {
- propsData,
- mocks: { $apollo, clusterAgent },
- stubs: { GlTab },
- });
+ wrapper = extendedWrapper(
+ shallowMount(ClusterAgentShow, {
+ propsData,
+ mocks: { $apollo, clusterAgent },
+ slots,
+ stubs: { GlTab },
+ }),
+ );
};
- const findCreatedText = () => wrapper.find('[data-testid="cluster-agent-create-info"]').text();
- const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
- const findPaginationButtons = () => wrapper.find(GlKeysetPagination);
- const findTokenCount = () => wrapper.find('[data-testid="cluster-agent-token-count"]').text();
+ const findCreatedText = () => wrapper.findByTestId('cluster-agent-create-info').text();
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findPaginationButtons = () => wrapper.findComponent(GlKeysetPagination);
+ const findTokenCount = () => wrapper.findByTestId('cluster-agent-token-count').text();
+ const findEESecurityTabSlot = () => wrapper.findByTestId('ee-security-tab');
afterEach(() => {
wrapper.destroy();
@@ -87,7 +95,7 @@ describe('ClusterAgentShow', () => {
});
it('renders token table', () => {
- expect(wrapper.find(TokenTable).exists()).toBe(true);
+ expect(wrapper.findComponent(TokenTable).exists()).toBe(true);
});
it('should not render pagination buttons when there are no additional pages', () => {
@@ -188,8 +196,27 @@ describe('ClusterAgentShow', () => {
});
it('displays an alert message', () => {
- expect(wrapper.find(GlAlert).exists()).toBe(true);
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(true);
expect(wrapper.text()).toContain(ClusterAgentShow.i18n.loadingError);
});
});
+
+ describe('ee-security-tab slot', () => {
+ it('does not display when a slot is not passed in', async () => {
+ createWrapperWithoutApollo({ clusterAgent: defaultClusterAgent });
+ await nextTick();
+ expect(findEESecurityTabSlot().exists()).toBe(false);
+ });
+
+ it('does display when a slot is passed in', async () => {
+ createWrapperWithoutApollo({
+ clusterAgent: defaultClusterAgent,
+ slots: {
+ 'ee-security-tab': `<gl-tab data-testid="ee-security-tab">Security Tab!</gl-tab>`,
+ },
+ });
+ await nextTick();
+ expect(findEESecurityTabSlot().exists()).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js b/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js
index e2726b93ea5..41bd492148e 100644
--- a/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js
+++ b/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js
@@ -1,18 +1,20 @@
-import { GlModal } from '@gitlab/ui';
+import { GlModal, GlSprintf } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import { stubComponent } from 'helpers/stub_component';
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;
- const createComponent = (props = {}) => {
+ const createComponent = ({ props = {}, stubs = {} } = {}) => {
wrapper = mount(RemoveClusterConfirmation, {
propsData: {
clusterPath: 'clusterPath',
clusterName: 'clusterName',
...props,
},
+ stubs,
});
};
@@ -27,35 +29,44 @@ describe('Remove cluster confirmation modal', () => {
});
describe('split button dropdown', () => {
- const findModal = () => wrapper.find(GlModal).vm;
- const findSplitButton = () => wrapper.find(SplitButton);
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findSplitButton = () => wrapper.findComponent(SplitButton);
beforeEach(() => {
- createComponent({ clusterName: 'my-test-cluster' });
- jest.spyOn(findModal(), 'show').mockReturnValue();
+ createComponent({
+ props: { clusterName: 'my-test-cluster' },
+ stubs: { GlSprintf, GlModal: stubComponent(GlModal) },
+ });
+ jest.spyOn(findModal().vm, 'show').mockReturnValue();
});
- it('opens modal with "cleanup" option', () => {
+ it('opens modal with "cleanup" option', async () => {
findSplitButton().vm.$emit('remove-cluster-and-cleanup');
- return wrapper.vm.$nextTick().then(() => {
- expect(findModal().show).toHaveBeenCalled();
- expect(wrapper.vm.confirmCleanup).toEqual(true);
- });
+ await wrapper.vm.$nextTick();
+
+ expect(findModal().vm.show).toHaveBeenCalled();
+ expect(wrapper.vm.confirmCleanup).toEqual(true);
+ expect(findModal().html()).toContain(
+ '<strong>To remove your integration and resources, type <code>my-test-cluster</code> to confirm:</strong>',
+ );
});
- it('opens modal without "cleanup" option', () => {
+ it('opens modal without "cleanup" option', async () => {
findSplitButton().vm.$emit('remove-cluster');
- return wrapper.vm.$nextTick().then(() => {
- expect(findModal().show).toHaveBeenCalled();
- expect(wrapper.vm.confirmCleanup).toEqual(false);
- });
+ await wrapper.vm.$nextTick();
+
+ expect(findModal().vm.show).toHaveBeenCalled();
+ expect(wrapper.vm.confirmCleanup).toEqual(false);
+ expect(findModal().html()).toContain(
+ '<strong>To remove your integration, type <code>my-test-cluster</code> to confirm:</strong>',
+ );
});
describe('with cluster management project', () => {
beforeEach(() => {
- createComponent({ hasManagementProject: true });
+ createComponent({ props: { hasManagementProject: true } });
});
it('renders regular button instead', () => {
diff --git a/spec/frontend/clusters_list/components/agent_empty_state_spec.js b/spec/frontend/clusters_list/components/agent_empty_state_spec.js
index a548721588e..38f0e0ba2c4 100644
--- a/spec/frontend/clusters_list/components/agent_empty_state_spec.js
+++ b/spec/frontend/clusters_list/components/agent_empty_state_spec.js
@@ -1,13 +1,12 @@
import { GlAlert, GlEmptyState, GlSprintf } from '@gitlab/ui';
import AgentEmptyState from '~/clusters_list/components/agent_empty_state.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { helpPagePath } from '~/helpers/help_page_helper';
const emptyStateImage = '/path/to/image';
const projectPath = 'path/to/project';
-const agentDocsUrl = 'path/to/agentDocs';
-const installDocsUrl = 'path/to/installDocs';
-const getStartedDocsUrl = 'path/to/getStartedDocs';
-const integrationDocsUrl = 'path/to/integrationDocs';
+const multipleClustersDocsUrl = helpPagePath('user/project/clusters/multiple_kubernetes_clusters');
+const installDocsUrl = helpPagePath('administration/clusters/kas');
describe('AgentEmptyStateComponent', () => {
let wrapper;
@@ -18,14 +17,10 @@ describe('AgentEmptyStateComponent', () => {
const provideData = {
emptyStateImage,
projectPath,
- agentDocsUrl,
- installDocsUrl,
- getStartedDocsUrl,
- integrationDocsUrl,
};
const findConfigurationsAlert = () => wrapper.findComponent(GlAlert);
- const findAgentDocsLink = () => wrapper.findByTestId('agent-docs-link');
+ const findMultipleClustersDocsLink = () => wrapper.findByTestId('multiple-clusters-docs-link');
const findInstallDocsLink = () => wrapper.findByTestId('install-docs-link');
const findIntegrationButton = () => wrapper.findByTestId('integration-primary-button');
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
@@ -41,12 +36,11 @@ describe('AgentEmptyStateComponent', () => {
afterEach(() => {
if (wrapper) {
wrapper.destroy();
- wrapper = null;
}
});
it('renders correct href attributes for the links', () => {
- expect(findAgentDocsLink().attributes('href')).toBe(agentDocsUrl);
+ expect(findMultipleClustersDocsLink().attributes('href')).toBe(multipleClustersDocsUrl);
expect(findInstallDocsLink().attributes('href')).toBe(installDocsUrl);
});
diff --git a/spec/frontend/clusters_list/components/agent_table_spec.js b/spec/frontend/clusters_list/components/agent_table_spec.js
index e3b90584f29..a6d76b069cf 100644
--- a/spec/frontend/clusters_list/components/agent_table_spec.js
+++ b/spec/frontend/clusters_list/components/agent_table_spec.js
@@ -1,4 +1,4 @@
-import { GlButton, GlLink, GlIcon } from '@gitlab/ui';
+import { GlLink, GlIcon } from '@gitlab/ui';
import AgentTable from '~/clusters_list/components/agent_table.vue';
import { ACTIVE_CONNECTION_TIME } from '~/clusters_list/constants';
import { mountExtended } from 'helpers/vue_test_utils_helper';
@@ -47,7 +47,6 @@ const propsData = {
},
],
};
-const provideData = { integrationDocsUrl: 'path/to/integrationDocs' };
describe('AgentTable', () => {
let wrapper;
@@ -60,7 +59,7 @@ describe('AgentTable', () => {
wrapper.findAllByTestId('cluster-agent-configuration-link').at(at);
beforeEach(() => {
- wrapper = mountExtended(AgentTable, { propsData, provide: provideData });
+ wrapper = mountExtended(AgentTable, { propsData });
});
afterEach(() => {
@@ -70,10 +69,6 @@ describe('AgentTable', () => {
}
});
- it('displays header button', () => {
- expect(wrapper.find(GlButton).text()).toBe('Install a new GitLab Agent');
- });
-
describe('agent table', () => {
it.each`
agentName | link | lineNumber
diff --git a/spec/frontend/clusters_list/components/agents_spec.js b/spec/frontend/clusters_list/components/agents_spec.js
index 54d5ae94172..2dec7cdc973 100644
--- a/spec/frontend/clusters_list/components/agents_spec.js
+++ b/spec/frontend/clusters_list/components/agents_spec.js
@@ -14,7 +14,7 @@ localVue.use(VueApollo);
describe('Agents', () => {
let wrapper;
- const propsData = {
+ const defaultProps = {
defaultBranchName: 'default',
};
const provideData = {
@@ -22,12 +22,12 @@ describe('Agents', () => {
kasAddress: 'kas.example.com',
};
- const createWrapper = ({ agents = [], pageInfo = null, trees = [] }) => {
+ const createWrapper = ({ props = {}, agents = [], pageInfo = null, trees = [], count = 0 }) => {
const provide = provideData;
const apolloQueryResponse = {
data: {
project: {
- clusterAgents: { nodes: agents, pageInfo, tokens: { nodes: [] } },
+ clusterAgents: { nodes: agents, pageInfo, tokens: { nodes: [] }, count },
repository: { tree: { trees: { nodes: trees, pageInfo } } },
},
},
@@ -40,7 +40,10 @@ describe('Agents', () => {
wrapper = shallowMount(Agents, {
localVue,
apolloProvider,
- propsData,
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
provide: provideData,
});
@@ -54,7 +57,6 @@ describe('Agents', () => {
afterEach(() => {
if (wrapper) {
wrapper.destroy();
- wrapper = null;
}
});
@@ -81,6 +83,8 @@ describe('Agents', () => {
},
];
+ const count = 2;
+
const trees = [
{
name: 'agent-2',
@@ -121,7 +125,7 @@ describe('Agents', () => {
];
beforeEach(() => {
- return createWrapper({ agents, trees });
+ return createWrapper({ agents, count, trees });
});
it('should render agent table', () => {
@@ -133,6 +137,10 @@ describe('Agents', () => {
expect(findAgentTable().props('agents')).toMatchObject(expectedAgentsList);
});
+ it('should emit agents count to the parent component', () => {
+ expect(wrapper.emitted().onAgentsLoad).toEqual([[count]]);
+ });
+
describe('when the agent has recently connected tokens', () => {
it('should set agent status to active', () => {
expect(findAgentTable().props('agents')).toMatchObject(expectedAgentsList);
@@ -180,6 +188,20 @@ describe('Agents', () => {
it('should pass pageInfo to the pagination component', () => {
expect(findPaginationButtons().props()).toMatchObject(pageInfo);
});
+
+ describe('when limit is passed from the parent component', () => {
+ beforeEach(() => {
+ return createWrapper({
+ props: { limit: 6 },
+ agents,
+ pageInfo,
+ });
+ });
+
+ it('should not render pagination buttons', () => {
+ expect(findPaginationButtons().exists()).toBe(false);
+ });
+ });
});
});
@@ -234,7 +256,11 @@ describe('Agents', () => {
};
beforeEach(() => {
- wrapper = shallowMount(Agents, { mocks, propsData, provide: provideData });
+ wrapper = shallowMount(Agents, {
+ mocks,
+ propsData: defaultProps,
+ provide: provideData,
+ });
return wrapper.vm.$nextTick();
});
diff --git a/spec/frontend/clusters_list/components/clusters_actions_spec.js b/spec/frontend/clusters_list/components/clusters_actions_spec.js
new file mode 100644
index 00000000000..cb8303ca4b2
--- /dev/null
+++ b/spec/frontend/clusters_list/components/clusters_actions_spec.js
@@ -0,0 +1,55 @@
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ClustersActions from '~/clusters_list/components/clusters_actions.vue';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { INSTALL_AGENT_MODAL_ID, CLUSTERS_ACTIONS } from '~/clusters_list/constants';
+
+describe('ClustersActionsComponent', () => {
+ let wrapper;
+
+ const newClusterPath = 'path/to/create/cluster';
+ const addClusterPath = 'path/to/connect/existing/cluster';
+
+ const provideData = {
+ newClusterPath,
+ addClusterPath,
+ };
+
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findNewClusterLink = () => wrapper.findByTestId('new-cluster-link');
+ const findConnectClusterLink = () => wrapper.findByTestId('connect-cluster-link');
+ const findConnectNewAgentLink = () => wrapper.findByTestId('connect-new-agent-link');
+
+ beforeEach(() => {
+ wrapper = shallowMountExtended(ClustersActions, {
+ provide: provideData,
+ directives: {
+ GlModalDirective: createMockDirective(),
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders actions menu', () => {
+ expect(findDropdown().props('text')).toBe(CLUSTERS_ACTIONS.actionsButton);
+ });
+
+ it('renders a dropdown with 3 actions items', () => {
+ expect(findDropdownItems()).toHaveLength(3);
+ });
+
+ it('renders correct href attributes for the links', () => {
+ expect(findNewClusterLink().attributes('href')).toBe(newClusterPath);
+ expect(findConnectClusterLink().attributes('href')).toBe(addClusterPath);
+ });
+
+ it('renders correct modal id for the agent link', () => {
+ const binding = getBinding(findConnectNewAgentLink().element, 'gl-modal-directive');
+
+ expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
+ });
+});
diff --git a/spec/frontend/clusters_list/components/clusters_empty_state_spec.js b/spec/frontend/clusters_list/components/clusters_empty_state_spec.js
new file mode 100644
index 00000000000..f7e1791d0f7
--- /dev/null
+++ b/spec/frontend/clusters_list/components/clusters_empty_state_spec.js
@@ -0,0 +1,104 @@
+import { GlEmptyState, GlButton } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ClustersEmptyState from '~/clusters_list/components/clusters_empty_state.vue';
+import ClusterStore from '~/clusters_list/store';
+
+const clustersEmptyStateImage = 'path/to/svg';
+const newClusterPath = '/path/to/connect/cluster';
+const emptyStateHelpText = 'empty state text';
+const canAddCluster = true;
+
+describe('ClustersEmptyStateComponent', () => {
+ let wrapper;
+
+ const propsData = {
+ isChildComponent: false,
+ };
+
+ const provideData = {
+ clustersEmptyStateImage,
+ emptyStateHelpText: null,
+ newClusterPath,
+ };
+
+ const entryData = {
+ canAddCluster,
+ };
+
+ const findButton = () => wrapper.findComponent(GlButton);
+ const findEmptyStateText = () => wrapper.findByTestId('clusters-empty-state-text');
+
+ beforeEach(() => {
+ wrapper = shallowMountExtended(ClustersEmptyState, {
+ store: ClusterStore(entryData),
+ propsData,
+ provide: provideData,
+ stubs: { GlEmptyState },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when the component is loaded independently', () => {
+ it('should render the action button', () => {
+ expect(findButton().exists()).toBe(true);
+ });
+ });
+
+ describe('when the help text is not provided', () => {
+ it('should not render the empty state text', () => {
+ expect(findEmptyStateText().exists()).toBe(false);
+ });
+ });
+
+ describe('when the component is loaded as a child component', () => {
+ beforeEach(() => {
+ propsData.isChildComponent = true;
+ wrapper = shallowMountExtended(ClustersEmptyState, {
+ store: ClusterStore(entryData),
+ propsData,
+ provide: provideData,
+ });
+ });
+
+ afterEach(() => {
+ propsData.isChildComponent = false;
+ });
+
+ it('should not render the action button', () => {
+ expect(findButton().exists()).toBe(false);
+ });
+ });
+
+ describe('when the help text is provided', () => {
+ beforeEach(() => {
+ provideData.emptyStateHelpText = emptyStateHelpText;
+ wrapper = shallowMountExtended(ClustersEmptyState, {
+ store: ClusterStore(entryData),
+ propsData,
+ provide: provideData,
+ });
+ });
+
+ it('should show the empty state text', () => {
+ expect(findEmptyStateText().text()).toBe(emptyStateHelpText);
+ });
+ });
+
+ describe('when the user cannot add clusters', () => {
+ entryData.canAddCluster = false;
+ beforeEach(() => {
+ wrapper = shallowMountExtended(ClustersEmptyState, {
+ store: ClusterStore(entryData),
+ propsData,
+ provide: provideData,
+ stubs: { GlEmptyState },
+ });
+ });
+ it('should disable the button', () => {
+ expect(findButton().props('disabled')).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/clusters_list/components/clusters_main_view_spec.js b/spec/frontend/clusters_list/components/clusters_main_view_spec.js
new file mode 100644
index 00000000000..c2233e5d39c
--- /dev/null
+++ b/spec/frontend/clusters_list/components/clusters_main_view_spec.js
@@ -0,0 +1,82 @@
+import { GlTabs, GlTab } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ClustersMainView from '~/clusters_list/components/clusters_main_view.vue';
+import InstallAgentModal from '~/clusters_list/components/install_agent_modal.vue';
+import {
+ AGENT,
+ CERTIFICATE_BASED,
+ CLUSTERS_TABS,
+ MAX_CLUSTERS_LIST,
+ MAX_LIST_COUNT,
+} from '~/clusters_list/constants';
+
+const defaultBranchName = 'default-branch';
+
+describe('ClustersMainViewComponent', () => {
+ let wrapper;
+
+ const propsData = {
+ defaultBranchName,
+ };
+
+ beforeEach(() => {
+ wrapper = shallowMountExtended(ClustersMainView, {
+ propsData,
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findTabs = () => wrapper.findComponent(GlTabs);
+ const findAllTabs = () => wrapper.findAllComponents(GlTab);
+ const findGlTabAtIndex = (index) => findAllTabs().at(index);
+ const findComponent = () => wrapper.findByTestId('clusters-tab-component');
+ const findModal = () => wrapper.findComponent(InstallAgentModal);
+
+ it('renders `GlTabs` with `syncActiveTabWithQueryParams` and `queryParamName` props set', () => {
+ expect(findTabs().exists()).toBe(true);
+ expect(findTabs().props('syncActiveTabWithQueryParams')).toBe(true);
+ });
+
+ it('renders correct number of tabs', () => {
+ expect(findAllTabs()).toHaveLength(CLUSTERS_TABS.length);
+ });
+
+ it('passes child-component param to the component', () => {
+ expect(findComponent().props('defaultBranchName')).toBe(defaultBranchName);
+ });
+
+ it('passes correct max-agents param to the modal', () => {
+ expect(findModal().props('maxAgents')).toBe(MAX_CLUSTERS_LIST);
+ });
+
+ describe('tabs', () => {
+ it.each`
+ tabTitle | queryParamValue | lineNumber
+ ${'All'} | ${'all'} | ${0}
+ ${'Agent'} | ${AGENT} | ${1}
+ ${'Certificate based'} | ${CERTIFICATE_BASED} | ${2}
+ `(
+ 'renders correct tab title and query param value',
+ ({ tabTitle, queryParamValue, lineNumber }) => {
+ expect(findGlTabAtIndex(lineNumber).attributes('title')).toBe(tabTitle);
+ expect(findGlTabAtIndex(lineNumber).props('queryParamValue')).toBe(queryParamValue);
+ },
+ );
+ });
+
+ describe('when the child component emits the tab change event', () => {
+ beforeEach(() => {
+ findComponent().vm.$emit('changeTab', AGENT);
+ });
+ it('changes the tab', () => {
+ expect(findTabs().attributes('value')).toBe('1');
+ });
+
+ it('passes correct max-agents param to the modal', () => {
+ expect(findModal().props('maxAgents')).toBe(MAX_LIST_COUNT);
+ });
+ });
+});
diff --git a/spec/frontend/clusters_list/components/clusters_spec.js b/spec/frontend/clusters_list/components/clusters_spec.js
index 941a3adb625..a34202c789d 100644
--- a/spec/frontend/clusters_list/components/clusters_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_spec.js
@@ -8,6 +8,7 @@ import * as Sentry from '@sentry/browser';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import Clusters from '~/clusters_list/components/clusters.vue';
+import ClustersEmptyState from '~/clusters_list/components/clusters_empty_state.vue';
import ClusterStore from '~/clusters_list/store';
import axios from '~/lib/utils/axios_utils';
import { apiData } from '../mock_data';
@@ -18,26 +19,38 @@ describe('Clusters', () => {
let wrapper;
const endpoint = 'some/endpoint';
+ const totalClustersNumber = 6;
+ const clustersEmptyStateImage = 'path/to/svg';
+ const emptyStateHelpText = null;
+ const newClusterPath = '/path/to/new/cluster';
const entryData = {
endpoint,
imgTagsAwsText: 'AWS Icon',
imgTagsDefaultText: 'Default Icon',
imgTagsGcpText: 'GCP Icon',
+ totalClusters: totalClustersNumber,
};
- const findLoader = () => wrapper.find(GlLoadingIcon);
- const findPaginatedButtons = () => wrapper.find(GlPagination);
- const findTable = () => wrapper.find(GlTable);
+ const provideData = {
+ clustersEmptyStateImage,
+ emptyStateHelpText,
+ newClusterPath,
+ };
+
+ const findLoader = () => wrapper.findComponent(GlLoadingIcon);
+ const findPaginatedButtons = () => wrapper.findComponent(GlPagination);
+ const findTable = () => wrapper.findComponent(GlTable);
const findStatuses = () => findTable().findAll('.js-status');
+ const findEmptyState = () => wrapper.findComponent(ClustersEmptyState);
const mockPollingApi = (response, body, header) => {
mock.onGet(`${endpoint}?page=${header['x-page']}`).reply(response, body, header);
};
- const mountWrapper = () => {
+ const createWrapper = ({ propsData = {} }) => {
store = ClusterStore(entryData);
- wrapper = mount(Clusters, { store });
+ wrapper = mount(Clusters, { propsData, provide: provideData, store, stubs: { GlTable } });
return axios.waitForAll();
};
@@ -57,7 +70,7 @@ describe('Clusters', () => {
mock = new MockAdapter(axios);
mockPollingApi(200, apiData, paginationHeader());
- return mountWrapper();
+ return createWrapper({});
});
afterEach(() => {
@@ -70,7 +83,6 @@ describe('Clusters', () => {
describe('when data is loading', () => {
beforeEach(() => {
wrapper.vm.$store.state.loadingClusters = true;
- return wrapper.vm.$nextTick();
});
it('displays a loader instead of the table while loading', () => {
@@ -79,23 +91,29 @@ describe('Clusters', () => {
});
});
- it('displays a table component', () => {
- expect(findTable().exists()).toBe(true);
+ describe('when clusters are present', () => {
+ it('displays a table component', () => {
+ expect(findTable().exists()).toBe(true);
+ });
});
- it('renders the correct table headers', () => {
- const tableHeaders = wrapper.vm.fields;
- const headers = findTable().findAll('th');
-
- expect(headers.length).toBe(tableHeaders.length);
-
- tableHeaders.forEach((headerText, i) =>
- expect(headers.at(i).text()).toEqual(headerText.label),
- );
+ describe('when there are no clusters', () => {
+ beforeEach(() => {
+ wrapper.vm.$store.state.totalClusters = 0;
+ });
+ it('should render empty state', () => {
+ expect(findEmptyState().exists()).toBe(true);
+ });
});
- it('should stack on smaller devices', () => {
- expect(findTable().classes()).toContain('b-table-stacked-md');
+ describe('when is loaded as a child component', () => {
+ beforeEach(() => {
+ createWrapper({ limit: 6 });
+ });
+
+ it("shouldn't render pagination buttons", () => {
+ expect(findPaginatedButtons().exists()).toBe(false);
+ });
});
});
@@ -240,7 +258,7 @@ describe('Clusters', () => {
beforeEach(() => {
mockPollingApi(200, apiData, paginationHeader(totalFirstPage, perPage, 1));
- return mountWrapper();
+ return createWrapper({});
});
it('should load to page 1 with header values', () => {
diff --git a/spec/frontend/clusters_list/components/clusters_view_all_spec.js b/spec/frontend/clusters_list/components/clusters_view_all_spec.js
new file mode 100644
index 00000000000..6ef56beddee
--- /dev/null
+++ b/spec/frontend/clusters_list/components/clusters_view_all_spec.js
@@ -0,0 +1,243 @@
+import { GlCard, GlLoadingIcon, GlButton, GlSprintf, GlBadge } from '@gitlab/ui';
+import { createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ClustersViewAll from '~/clusters_list/components/clusters_view_all.vue';
+import Agents from '~/clusters_list/components/agents.vue';
+import Clusters from '~/clusters_list/components/clusters.vue';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import {
+ AGENT,
+ CERTIFICATE_BASED,
+ AGENT_CARD_INFO,
+ CERTIFICATE_BASED_CARD_INFO,
+ MAX_CLUSTERS_LIST,
+ INSTALL_AGENT_MODAL_ID,
+} from '~/clusters_list/constants';
+import { sprintf } from '~/locale';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+const addClusterPath = '/path/to/add/cluster';
+const defaultBranchName = 'default-branch';
+
+describe('ClustersViewAllComponent', () => {
+ let wrapper;
+
+ const event = {
+ preventDefault: jest.fn(),
+ };
+
+ const propsData = {
+ defaultBranchName,
+ };
+
+ const provideData = {
+ addClusterPath,
+ };
+
+ const entryData = {
+ loadingClusters: false,
+ totalClusters: 0,
+ };
+
+ const findCards = () => wrapper.findAllComponents(GlCard);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findAgentsComponent = () => wrapper.findComponent(Agents);
+ const findClustersComponent = () => wrapper.findComponent(Clusters);
+ const findCardsContainer = () => wrapper.findByTestId('clusters-cards-container');
+ const findAgentCardTitle = () => wrapper.findByTestId('agent-card-title');
+ const findRecommendedBadge = () => wrapper.findComponent(GlBadge);
+ const findClustersCardTitle = () => wrapper.findByTestId('clusters-card-title');
+ const findFooterButton = (line) => findCards().at(line).findComponent(GlButton);
+
+ const createStore = (initialState) =>
+ new Vuex.Store({
+ state: initialState,
+ });
+
+ const createWrapper = ({ initialState }) => {
+ wrapper = shallowMountExtended(ClustersViewAll, {
+ localVue,
+ store: createStore(initialState),
+ propsData,
+ provide: provideData,
+ directives: {
+ GlModalDirective: createMockDirective(),
+ },
+ stubs: { GlCard, GlSprintf },
+ });
+ };
+
+ beforeEach(() => {
+ createWrapper({ initialState: entryData });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when agents and clusters are not loaded', () => {
+ const initialState = {
+ loadingClusters: true,
+ totalClusters: 0,
+ };
+ beforeEach(() => {
+ createWrapper({ initialState });
+ });
+
+ it('should show the loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+ });
+
+ describe('when both agents and clusters are loaded', () => {
+ beforeEach(() => {
+ findAgentsComponent().vm.$emit('onAgentsLoad', 6);
+ });
+
+ it("shouldn't show the loading icon", () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
+ it('should make content visible', () => {
+ expect(findCardsContainer().isVisible()).toBe(true);
+ });
+
+ it('should render 2 cards', () => {
+ expect(findCards().length).toBe(2);
+ });
+ });
+
+ describe('agents card', () => {
+ it('should show recommended badge', () => {
+ expect(findRecommendedBadge().exists()).toBe(true);
+ });
+
+ it('should render Agents component', () => {
+ expect(findAgentsComponent().exists()).toBe(true);
+ });
+
+ it('should pass the limit prop', () => {
+ expect(findAgentsComponent().props('limit')).toBe(MAX_CLUSTERS_LIST);
+ });
+
+ it('should pass the default-branch-name prop', () => {
+ expect(findAgentsComponent().props('defaultBranchName')).toBe(defaultBranchName);
+ });
+
+ describe('when there are no agents', () => {
+ it('should show the empty title', () => {
+ expect(findAgentCardTitle().text()).toBe(AGENT_CARD_INFO.emptyTitle);
+ });
+
+ it('should show install new Agent button in the footer', () => {
+ expect(findFooterButton(0).exists()).toBe(true);
+ });
+
+ it('should render correct modal id for the agent link', () => {
+ const binding = getBinding(findFooterButton(0).element, 'gl-modal-directive');
+
+ expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
+ });
+ });
+
+ describe('when the agents are present', () => {
+ const findFooterLink = () => wrapper.findByTestId('agents-tab-footer-link');
+ const agentsNumber = 7;
+
+ beforeEach(() => {
+ findAgentsComponent().vm.$emit('onAgentsLoad', agentsNumber);
+ });
+
+ it('should show the correct title', () => {
+ expect(findAgentCardTitle().text()).toBe(
+ sprintf(AGENT_CARD_INFO.title, { number: MAX_CLUSTERS_LIST, total: agentsNumber }),
+ );
+ });
+
+ it('should show the link to the Agents tab in the footer', () => {
+ expect(findFooterLink().exists()).toBe(true);
+ expect(findFooterLink().text()).toBe(
+ sprintf(AGENT_CARD_INFO.footerText, { number: agentsNumber }),
+ );
+ expect(findFooterLink().attributes('href')).toBe(`?tab=${AGENT}`);
+ });
+
+ describe('when clicking on the footer link', () => {
+ beforeEach(() => {
+ findFooterLink().vm.$emit('click', event);
+ });
+
+ it('should trigger tab change', () => {
+ expect(wrapper.emitted('changeTab')).toEqual([[AGENT]]);
+ });
+ });
+ });
+ });
+
+ describe('clusters tab', () => {
+ it('should pass the limit prop', () => {
+ expect(findClustersComponent().props('limit')).toBe(MAX_CLUSTERS_LIST);
+ });
+
+ it('should pass the is-child-component prop', () => {
+ expect(findClustersComponent().props('isChildComponent')).toBe(true);
+ });
+
+ describe('when there are no clusters', () => {
+ it('should show the empty title', () => {
+ expect(findClustersCardTitle().text()).toBe(CERTIFICATE_BASED_CARD_INFO.emptyTitle);
+ });
+
+ it('should show install new Agent button in the footer', () => {
+ expect(findFooterButton(1).exists()).toBe(true);
+ });
+
+ it('should render correct href for the button in the footer', () => {
+ expect(findFooterButton(1).attributes('href')).toBe(addClusterPath);
+ });
+ });
+
+ describe('when the clusters are present', () => {
+ const findFooterLink = () => wrapper.findByTestId('clusters-tab-footer-link');
+
+ const clustersNumber = 7;
+ const initialState = {
+ loadingClusters: false,
+ totalClusters: clustersNumber,
+ };
+
+ beforeEach(() => {
+ createWrapper({ initialState });
+ });
+
+ it('should show the correct title', () => {
+ expect(findClustersCardTitle().text()).toBe(
+ sprintf(CERTIFICATE_BASED_CARD_INFO.title, {
+ number: MAX_CLUSTERS_LIST,
+ total: clustersNumber,
+ }),
+ );
+ });
+
+ it('should show the link to the Clusters tab in the footer', () => {
+ expect(findFooterLink().exists()).toBe(true);
+ expect(findFooterLink().text()).toBe(
+ sprintf(CERTIFICATE_BASED_CARD_INFO.footerText, { number: clustersNumber }),
+ );
+ });
+
+ describe('when clicking on the footer link', () => {
+ beforeEach(() => {
+ findFooterLink().vm.$emit('click', event);
+ });
+
+ it('should trigger tab change', () => {
+ expect(wrapper.emitted('changeTab')).toEqual([[CERTIFICATE_BASED]]);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/clusters_list/components/install_agent_modal_spec.js b/spec/frontend/clusters_list/components/install_agent_modal_spec.js
index 98ca5e05b3f..6c2ea45b99b 100644
--- a/spec/frontend/clusters_list/components/install_agent_modal_spec.js
+++ b/spec/frontend/clusters_list/components/install_agent_modal_spec.js
@@ -3,7 +3,8 @@ import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import AvailableAgentsDropdown from '~/clusters_list/components/available_agents_dropdown.vue';
import InstallAgentModal from '~/clusters_list/components/install_agent_modal.vue';
-import { I18N_INSTALL_AGENT_MODAL } from '~/clusters_list/constants';
+import { I18N_INSTALL_AGENT_MODAL, MAX_LIST_COUNT } from '~/clusters_list/constants';
+import getAgentsQuery from '~/clusters_list/graphql/queries/get_agents.query.graphql';
import createAgentMutation from '~/clusters_list/graphql/mutations/create_agent.mutation.graphql';
import createAgentTokenMutation from '~/clusters_list/graphql/mutations/create_agent_token.mutation.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -14,12 +15,17 @@ import {
createAgentErrorResponse,
createAgentTokenResponse,
createAgentTokenErrorResponse,
+ getAgentResponse,
} from '../mocks/apollo';
import ModalStub from '../stubs';
const localVue = createLocalVue();
localVue.use(VueApollo);
+const projectPath = 'path/to/project';
+const defaultBranchName = 'default';
+const maxAgents = MAX_LIST_COUNT;
+
describe('InstallAgentModal', () => {
let wrapper;
let apolloProvider;
@@ -45,10 +51,15 @@ describe('InstallAgentModal', () => {
const createWrapper = () => {
const provide = {
- projectPath: 'path/to/project',
+ projectPath,
kasAddress: 'kas.example.com',
};
+ const propsData = {
+ defaultBranchName,
+ maxAgents,
+ };
+
wrapper = shallowMount(InstallAgentModal, {
attachTo: document.body,
stubs: {
@@ -57,11 +68,26 @@ describe('InstallAgentModal', () => {
localVue,
apolloProvider,
provide,
+ propsData,
+ });
+ };
+
+ const writeQuery = () => {
+ apolloProvider.clients.defaultClient.cache.writeQuery({
+ query: getAgentsQuery,
+ variables: {
+ projectPath,
+ defaultBranchName,
+ first: MAX_LIST_COUNT,
+ last: null,
+ },
+ data: getAgentResponse.data,
});
};
const mockSelectedAgentResponse = () => {
createWrapper();
+ writeQuery();
wrapper.vm.setAgentName('agent-name');
findActionButton().vm.$emit('click');
@@ -95,7 +121,7 @@ describe('InstallAgentModal', () => {
it('renders a disabled next button', () => {
expect(findActionButton().isVisible()).toBe(true);
- expect(findActionButton().text()).toBe(i18n.next);
+ expect(findActionButton().text()).toBe(i18n.registerAgentButton);
expectDisabledAttribute(findActionButton(), true);
});
});
@@ -126,7 +152,7 @@ describe('InstallAgentModal', () => {
it('creates an agent and token', () => {
expect(createAgentHandler).toHaveBeenCalledWith({
- input: { name: 'agent-name', projectPath: 'path/to/project' },
+ input: { name: 'agent-name', projectPath },
});
expect(createAgentTokenHandler).toHaveBeenCalledWith({
@@ -134,9 +160,9 @@ describe('InstallAgentModal', () => {
});
});
- it('renders a done button', () => {
+ it('renders a close button', () => {
expect(findActionButton().isVisible()).toBe(true);
- expect(findActionButton().text()).toBe(i18n.done);
+ expect(findActionButton().text()).toBe(i18n.close);
expectDisabledAttribute(findActionButton(), false);
});
diff --git a/spec/frontend/clusters_list/mocks/apollo.js b/spec/frontend/clusters_list/mocks/apollo.js
index 27b71a0d4b5..1a7ef84a6d9 100644
--- a/spec/frontend/clusters_list/mocks/apollo.js
+++ b/spec/frontend/clusters_list/mocks/apollo.js
@@ -1,8 +1,29 @@
+const agent = {
+ id: 'agent-id',
+ name: 'agent-name',
+ webPath: 'agent-webPath',
+};
+const token = {
+ id: 'token-id',
+ lastUsedAt: null,
+};
+const tokens = {
+ nodes: [token],
+};
+const pageInfo = {
+ endCursor: '',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: '',
+};
+const count = 1;
+
export const createAgentResponse = {
data: {
createClusterAgent: {
clusterAgent: {
- id: 'agent-id',
+ ...agent,
+ tokens,
},
errors: [],
},
@@ -13,7 +34,8 @@ export const createAgentErrorResponse = {
data: {
createClusterAgent: {
clusterAgent: {
- id: 'agent-id',
+ ...agent,
+ tokens,
},
errors: ['could not create agent'],
},
@@ -23,9 +45,7 @@ export const createAgentErrorResponse = {
export const createAgentTokenResponse = {
data: {
clusterAgentTokenCreate: {
- token: {
- id: 'token-id',
- },
+ token,
secret: 'mock-agent-token',
errors: [],
},
@@ -35,11 +55,22 @@ export const createAgentTokenResponse = {
export const createAgentTokenErrorResponse = {
data: {
clusterAgentTokenCreate: {
- token: {
- id: 'token-id',
- },
+ token,
secret: 'mock-agent-token',
errors: ['could not create agent token'],
},
},
};
+
+export const getAgentResponse = {
+ data: {
+ project: {
+ clusterAgents: { nodes: [{ ...agent, tokens }], pageInfo, count },
+ repository: {
+ tree: {
+ trees: { nodes: [{ ...agent, path: null }], pageInfo },
+ },
+ },
+ },
+ },
+};
diff --git a/spec/frontend/clusters_list/store/mutations_spec.js b/spec/frontend/clusters_list/store/mutations_spec.js
index c0fe634a703..ae264eee449 100644
--- a/spec/frontend/clusters_list/store/mutations_spec.js
+++ b/spec/frontend/clusters_list/store/mutations_spec.js
@@ -26,7 +26,7 @@ describe('Admin statistics panel mutations', () => {
expect(state.clusters).toBe(apiData.clusters);
expect(state.clustersPerPage).toBe(paginationInformation.perPage);
expect(state.hasAncestorClusters).toBe(apiData.has_ancestor_clusters);
- expect(state.totalCulsters).toBe(paginationInformation.total);
+ expect(state.totalClusters).toBe(paginationInformation.total);
});
});
@@ -57,4 +57,12 @@ describe('Admin statistics panel mutations', () => {
expect(state.page).toBe(123);
});
});
+
+ describe(`${types.SET_CLUSTERS_PER_PAGE}`, () => {
+ it('changes clustersPerPage value', () => {
+ mutations[types.SET_CLUSTERS_PER_PAGE](state, 123);
+
+ expect(state.clustersPerPage).toBe(123);
+ });
+ });
});
diff --git a/spec/frontend/commit/pipelines/pipelines_table_spec.js b/spec/frontend/commit/pipelines/pipelines_table_spec.js
index 17f7be9d1d7..c376b58cc72 100644
--- a/spec/frontend/commit/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/commit/pipelines/pipelines_table_spec.js
@@ -1,4 +1,4 @@
-import { GlEmptyState, GlLoadingIcon, GlModal, GlTable } from '@gitlab/ui';
+import { GlEmptyState, GlLoadingIcon, GlModal, GlTableLite } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import fixture from 'test_fixtures/pipelines/pipelines.json';
@@ -6,8 +6,13 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
import PipelinesTable from '~/commit/pipelines/pipelines_table.vue';
+import { TOAST_MESSAGE } from '~/pipelines/constants';
import axios from '~/lib/utils/axios_utils';
+const $toast = {
+ show: jest.fn(),
+};
+
describe('Pipelines table in Commits and Merge requests', () => {
let wrapper;
let pipeline;
@@ -17,7 +22,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
const findRunPipelineBtnMobile = () => wrapper.findByTestId('run_pipeline_button_mobile');
const findLoadingState = () => wrapper.findComponent(GlLoadingIcon);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
- const findTable = () => wrapper.findComponent(GlTable);
+ const findTable = () => wrapper.findComponent(GlTableLite);
const findTableRows = () => wrapper.findAllByTestId('pipeline-table-row');
const findModal = () => wrapper.findComponent(GlModal);
@@ -30,6 +35,9 @@ describe('Pipelines table in Commits and Merge requests', () => {
errorStateSvgPath: 'foo',
...props,
},
+ mocks: {
+ $toast,
+ },
}),
);
};
@@ -178,6 +186,12 @@ describe('Pipelines table in Commits and Merge requests', () => {
await waitForPromises();
});
+ it('displays a toast message during pipeline creation', async () => {
+ await findRunPipelineBtn().trigger('click');
+
+ expect($toast.show).toHaveBeenCalledWith(TOAST_MESSAGE);
+ });
+
it('on desktop, shows a loading button', async () => {
await findRunPipelineBtn().trigger('click');
diff --git a/spec/frontend/confirm_modal_spec.js b/spec/frontend/confirm_modal_spec.js
index 8a12ff3a01f..5e5345cbd2b 100644
--- a/spec/frontend/confirm_modal_spec.js
+++ b/spec/frontend/confirm_modal_spec.js
@@ -72,7 +72,7 @@ describe('ConfirmModal', () => {
it('starts with only JsHooks', () => {
expect(findJsHooks()).toHaveLength(buttons.length);
- expect(findModal()).not.toExist();
+ expect(findModal()).toBe(null);
});
describe('when button clicked', () => {
@@ -87,7 +87,7 @@ describe('ConfirmModal', () => {
describe('GlModal', () => {
it('is rendered', () => {
- expect(findModal()).toExist();
+ expect(findModal()).not.toBe(null);
expect(modalIsHidden()).toBe(false);
});
diff --git a/spec/frontend/content_editor/components/content_editor_error_spec.js b/spec/frontend/content_editor/components/content_editor_alert_spec.js
index 8723fb5a338..2ddcd8f024e 100644
--- a/spec/frontend/content_editor/components/content_editor_error_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_alert_spec.js
@@ -1,11 +1,11 @@
import { GlAlert } from '@gitlab/ui';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import ContentEditorError from '~/content_editor/components/content_editor_error.vue';
+import ContentEditorAlert from '~/content_editor/components/content_editor_alert.vue';
import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue';
import { createTestEditor, emitEditorEvent } from '../test_utils';
-describe('content_editor/components/content_editor_error', () => {
+describe('content_editor/components/content_editor_alert', () => {
let wrapper;
let tiptapEditor;
@@ -14,7 +14,7 @@ describe('content_editor/components/content_editor_error', () => {
const createWrapper = async () => {
tiptapEditor = createTestEditor();
- wrapper = shallowMountExtended(ContentEditorError, {
+ wrapper = shallowMountExtended(ContentEditorAlert, {
provide: {
tiptapEditor,
},
@@ -28,22 +28,28 @@ describe('content_editor/components/content_editor_error', () => {
wrapper.destroy();
});
- it('renders error when content editor emits an error event', async () => {
- const error = 'error message';
+ it.each`
+ variant | message
+ ${'danger'} | ${'An error occurred'}
+ ${'warning'} | ${'A warning'}
+ `(
+ 'renders error when content editor emits an error event for variant: $variant',
+ async ({ message, variant }) => {
+ createWrapper();
- createWrapper();
-
- await emitEditorEvent({ tiptapEditor, event: 'error', params: { error } });
+ await emitEditorEvent({ tiptapEditor, event: 'alert', params: { message, variant } });
- expect(findErrorAlert().text()).toBe(error);
- });
+ expect(findErrorAlert().text()).toBe(message);
+ expect(findErrorAlert().attributes().variant).toBe(variant);
+ },
+ );
it('allows dismissing the error', async () => {
- const error = 'error message';
+ const message = 'error message';
createWrapper();
- await emitEditorEvent({ tiptapEditor, event: 'error', params: { error } });
+ await emitEditorEvent({ tiptapEditor, event: 'alert', params: { message } });
findErrorAlert().vm.$emit('dismiss');
diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js
index 3d1ef03083d..9a772c41e52 100644
--- a/spec/frontend/content_editor/components/content_editor_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_spec.js
@@ -3,7 +3,7 @@ import { EditorContent } from '@tiptap/vue-2';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ContentEditor from '~/content_editor/components/content_editor.vue';
-import ContentEditorError from '~/content_editor/components/content_editor_error.vue';
+import ContentEditorAlert from '~/content_editor/components/content_editor_alert.vue';
import ContentEditorProvider from '~/content_editor/components/content_editor_provider.vue';
import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue';
import FormattingBubbleMenu from '~/content_editor/components/formatting_bubble_menu.vue';
@@ -111,10 +111,10 @@ describe('ContentEditor', () => {
]);
});
- it('renders content_editor_error component', () => {
+ it('renders content_editor_alert component', () => {
createWrapper();
- expect(wrapper.findComponent(ContentEditorError).exists()).toBe(true);
+ expect(wrapper.findComponent(ContentEditorAlert).exists()).toBe(true);
});
describe('when loading content', () => {
diff --git a/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js b/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js
index e48f59f6d9c..6017a145a87 100644
--- a/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js
@@ -11,13 +11,13 @@ jest.mock('prosemirror-tables');
describe('content/components/wrappers/table_cell_base', () => {
let wrapper;
let editor;
- let getPos;
+ let node;
const createWrapper = async (propsData = { cellType: 'td' }) => {
wrapper = shallowMountExtended(TableCellBaseWrapper, {
propsData: {
editor,
- getPos,
+ node,
...propsData,
},
});
@@ -36,7 +36,7 @@ describe('content/components/wrappers/table_cell_base', () => {
const setCurrentPositionInCell = () => {
const { $cursor } = editor.state.selection;
- getPos.mockReturnValue($cursor.pos - $cursor.parentOffset - 1);
+ jest.spyOn($cursor, 'node').mockReturnValue(node);
};
const mockDropdownHide = () => {
/*
@@ -48,7 +48,7 @@ describe('content/components/wrappers/table_cell_base', () => {
};
beforeEach(() => {
- getPos = jest.fn();
+ node = {};
editor = createTestEditor({});
});
diff --git a/spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js b/spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js
index 5d26c44ba03..2aefbc77545 100644
--- a/spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js
@@ -6,19 +6,19 @@ import { createTestEditor } from '../../test_utils';
describe('content/components/wrappers/table_cell_body', () => {
let wrapper;
let editor;
- let getPos;
+ let node;
const createWrapper = async () => {
wrapper = shallowMount(TableCellBodyWrapper, {
propsData: {
editor,
- getPos,
+ node,
},
});
};
beforeEach(() => {
- getPos = jest.fn();
+ node = {};
editor = createTestEditor({});
});
@@ -30,7 +30,7 @@ describe('content/components/wrappers/table_cell_body', () => {
createWrapper();
expect(wrapper.findComponent(TableCellBaseWrapper).props()).toEqual({
editor,
- getPos,
+ node,
cellType: 'td',
});
});
diff --git a/spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js b/spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js
index e561191418d..e48df8734a6 100644
--- a/spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js
@@ -6,19 +6,19 @@ import { createTestEditor } from '../../test_utils';
describe('content/components/wrappers/table_cell_header', () => {
let wrapper;
let editor;
- let getPos;
+ let node;
const createWrapper = async () => {
wrapper = shallowMount(TableCellHeaderWrapper, {
propsData: {
editor,
- getPos,
+ node,
},
});
};
beforeEach(() => {
- getPos = jest.fn();
+ node = {};
editor = createTestEditor({});
});
@@ -30,7 +30,7 @@ describe('content/components/wrappers/table_cell_header', () => {
createWrapper();
expect(wrapper.findComponent(TableCellBaseWrapper).props()).toEqual({
editor,
- getPos,
+ node,
cellType: 'th',
});
});
diff --git a/spec/frontend/content_editor/extensions/attachment_spec.js b/spec/frontend/content_editor/extensions/attachment_spec.js
index d4f05a25bd6..d2d2cd98a78 100644
--- a/spec/frontend/content_editor/extensions/attachment_spec.js
+++ b/spec/frontend/content_editor/extensions/attachment_spec.js
@@ -74,10 +74,10 @@ describe('content_editor/extensions/attachment', () => {
});
it.each`
- eventType | propName | eventData | output
- ${'paste'} | ${'handlePaste'} | ${{ clipboardData: { files: [attachmentFile] } }} | ${true}
- ${'paste'} | ${'handlePaste'} | ${{ clipboardData: { files: [] } }} | ${undefined}
- ${'drop'} | ${'handleDrop'} | ${{ dataTransfer: { files: [attachmentFile] } }} | ${true}
+ eventType | propName | eventData | output
+ ${'paste'} | ${'handlePaste'} | ${{ clipboardData: { getData: jest.fn(), files: [attachmentFile] } }} | ${true}
+ ${'paste'} | ${'handlePaste'} | ${{ clipboardData: { getData: jest.fn(), files: [] } }} | ${undefined}
+ ${'drop'} | ${'handleDrop'} | ${{ dataTransfer: { getData: jest.fn(), files: [attachmentFile] } }} | ${true}
`('handles $eventType properly', ({ eventType, propName, eventData, output }) => {
const event = Object.assign(new Event(eventType), eventData);
const handled = tiptapEditor.view.someProp(propName, (eventHandler) => {
@@ -157,11 +157,11 @@ describe('content_editor/extensions/attachment', () => {
});
});
- it('emits an error event that includes an error message', (done) => {
+ it('emits an alert event that includes an error message', (done) => {
tiptapEditor.commands.uploadAttachment({ file: imageFile });
- tiptapEditor.on('error', ({ error }) => {
- expect(error).toBe('An error occurred while uploading the image. Please try again.');
+ tiptapEditor.on('alert', ({ message }) => {
+ expect(message).toBe('An error occurred while uploading the image. Please try again.');
done();
});
});
@@ -233,11 +233,11 @@ describe('content_editor/extensions/attachment', () => {
});
});
- it('emits an error event that includes an error message', (done) => {
+ it('emits an alert event that includes an error message', (done) => {
tiptapEditor.commands.uploadAttachment({ file: attachmentFile });
- tiptapEditor.on('error', ({ error }) => {
- expect(error).toBe('An error occurred while uploading the file. Please try again.');
+ tiptapEditor.on('alert', ({ message }) => {
+ expect(message).toBe('An error occurred while uploading the file. Please try again.');
done();
});
});
diff --git a/spec/frontend/content_editor/extensions/blockquote_spec.js b/spec/frontend/content_editor/extensions/blockquote_spec.js
index c5b5044352d..1644647ba69 100644
--- a/spec/frontend/content_editor/extensions/blockquote_spec.js
+++ b/spec/frontend/content_editor/extensions/blockquote_spec.js
@@ -1,19 +1,37 @@
-import { multilineInputRegex } from '~/content_editor/extensions/blockquote';
+import Blockquote from '~/content_editor/extensions/blockquote';
+import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils';
describe('content_editor/extensions/blockquote', () => {
- describe.each`
- input | matches
- ${'>>> '} | ${true}
- ${' >>> '} | ${true}
- ${'\t>>> '} | ${true}
- ${'>> '} | ${false}
- ${'>>>x '} | ${false}
- ${'> '} | ${false}
- `('multilineInputRegex', ({ input, matches }) => {
- it(`${matches ? 'matches' : 'does not match'}: "${input}"`, () => {
- const match = new RegExp(multilineInputRegex).test(input);
+ let tiptapEditor;
+ let doc;
+ let p;
+ let blockquote;
- expect(match).toBe(matches);
- });
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({ extensions: [Blockquote] });
+
+ ({
+ builders: { doc, p, blockquote },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ blockquote: { nodeType: Blockquote.name },
+ },
+ }));
+ });
+
+ it.each`
+ input | insertedNode
+ ${'>>> '} | ${() => blockquote({ multiline: true }, p())}
+ ${'> '} | ${() => blockquote(p())}
+ ${' >>> '} | ${() => blockquote({ multiline: true }, p())}
+ ${'>> '} | ${() => p()}
+ ${'>>>x '} | ${() => p()}
+ `('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => {
+ const expectedDoc = doc(insertedNode());
+
+ triggerNodeInputRule({ tiptapEditor, inputRuleText: input });
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
});
});
diff --git a/spec/frontend/content_editor/extensions/emoji_spec.js b/spec/frontend/content_editor/extensions/emoji_spec.js
index c1b8dc9bdbb..939c46e991a 100644
--- a/spec/frontend/content_editor/extensions/emoji_spec.js
+++ b/spec/frontend/content_editor/extensions/emoji_spec.js
@@ -1,6 +1,6 @@
import { initEmojiMock } from 'helpers/emoji';
import Emoji from '~/content_editor/extensions/emoji';
-import { createTestEditor, createDocBuilder } from '../test_utils';
+import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils';
describe('content_editor/extensions/emoji', () => {
let tiptapEditor;
@@ -28,18 +28,16 @@ describe('content_editor/extensions/emoji', () => {
describe('when typing a valid emoji input rule', () => {
it('inserts an emoji node', () => {
- const { view } = tiptapEditor;
- const { selection } = view.state;
const expectedDoc = doc(
p(
' ',
emoji({ moji: '❤', name: 'heart', title: 'heavy black heart', unicodeVersion: '1.1' }),
),
);
- // Triggers the event handler that input rules listen to
- view.someProp('handleTextInput', (f) => f(view, selection.from, selection.to, ':heart:'));
- expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true);
+ triggerNodeInputRule({ tiptapEditor, inputRuleText: ':heart:' });
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
});
});
diff --git a/spec/frontend/content_editor/extensions/frontmatter_spec.js b/spec/frontend/content_editor/extensions/frontmatter_spec.js
new file mode 100644
index 00000000000..517f6947b9a
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/frontmatter_spec.js
@@ -0,0 +1,30 @@
+import Frontmatter from '~/content_editor/extensions/frontmatter';
+import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils';
+
+describe('content_editor/extensions/frontmatter', () => {
+ let tiptapEditor;
+ let doc;
+ let p;
+
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({ extensions: [Frontmatter] });
+
+ ({
+ builders: { doc, p },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ frontmatter: { nodeType: Frontmatter.name },
+ },
+ }));
+ });
+
+ it('does not insert a frontmatter block when executing code block input rule', () => {
+ const expectedDoc = doc(p(''));
+ const inputRuleText = '``` ';
+
+ triggerNodeInputRule({ tiptapEditor, inputRuleText });
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ });
+});
diff --git a/spec/frontend/content_editor/extensions/horizontal_rule_spec.js b/spec/frontend/content_editor/extensions/horizontal_rule_spec.js
index a1bc7f0e8ed..322c04a42e1 100644
--- a/spec/frontend/content_editor/extensions/horizontal_rule_spec.js
+++ b/spec/frontend/content_editor/extensions/horizontal_rule_spec.js
@@ -1,20 +1,39 @@
-import { hrInputRuleRegExp } from '~/content_editor/extensions/horizontal_rule';
+import HorizontalRule from '~/content_editor/extensions/horizontal_rule';
+import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils';
describe('content_editor/extensions/horizontal_rule', () => {
- describe.each`
- input | matches
- ${'---'} | ${true}
- ${'--'} | ${false}
- ${'---x'} | ${false}
- ${' ---x'} | ${false}
- ${' --- '} | ${false}
- ${'x---x'} | ${false}
- ${'x---'} | ${false}
- `('hrInputRuleRegExp', ({ input, matches }) => {
- it(`${matches ? 'matches' : 'does not match'}: "${input}"`, () => {
- const match = new RegExp(hrInputRuleRegExp).test(input);
+ let tiptapEditor;
+ let doc;
+ let p;
+ let horizontalRule;
- expect(match).toBe(matches);
- });
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({ extensions: [HorizontalRule] });
+
+ ({
+ builders: { doc, p, horizontalRule },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ horizontalRule: { nodeType: HorizontalRule.name },
+ },
+ }));
+ });
+
+ it.each`
+ input | insertedNodes
+ ${'---'} | ${() => [p(), horizontalRule()]}
+ ${'--'} | ${() => [p()]}
+ ${'---x'} | ${() => [p()]}
+ ${' ---x'} | ${() => [p()]}
+ ${' --- '} | ${() => [p()]}
+ ${'x---x'} | ${() => [p()]}
+ ${'x---'} | ${() => [p()]}
+ `('with input=$input, then should insert a $insertedNode', ({ input, insertedNodes }) => {
+ const expectedDoc = doc(...insertedNodes());
+
+ triggerNodeInputRule({ tiptapEditor, inputRuleText: input });
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
});
});
diff --git a/spec/frontend/content_editor/extensions/inline_diff_spec.js b/spec/frontend/content_editor/extensions/inline_diff_spec.js
index 63cdf665e7f..99c559a20b1 100644
--- a/spec/frontend/content_editor/extensions/inline_diff_spec.js
+++ b/spec/frontend/content_editor/extensions/inline_diff_spec.js
@@ -1,27 +1,43 @@
-import { inputRegexAddition, inputRegexDeletion } from '~/content_editor/extensions/inline_diff';
+import InlineDiff from '~/content_editor/extensions/inline_diff';
+import { createTestEditor, createDocBuilder, triggerMarkInputRule } from '../test_utils';
describe('content_editor/extensions/inline_diff', () => {
- describe.each`
- inputRegex | description | input | matches
- ${inputRegexAddition} | ${'inputRegexAddition'} | ${'hello{+world+}'} | ${true}
- ${inputRegexAddition} | ${'inputRegexAddition'} | ${'hello{+ world +}'} | ${true}
- ${inputRegexAddition} | ${'inputRegexAddition'} | ${'hello {+ world+}'} | ${true}
- ${inputRegexAddition} | ${'inputRegexAddition'} | ${'{+hello world +}'} | ${true}
- ${inputRegexAddition} | ${'inputRegexAddition'} | ${'{+hello with \nnewline+}'} | ${false}
- ${inputRegexAddition} | ${'inputRegexAddition'} | ${'{+open only'} | ${false}
- ${inputRegexAddition} | ${'inputRegexAddition'} | ${'close only+}'} | ${false}
- ${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'hello{-world-}'} | ${true}
- ${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'hello{- world -}'} | ${true}
- ${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'hello {- world-}'} | ${true}
- ${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'{-hello world -}'} | ${true}
- ${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'{+hello with \nnewline+}'} | ${false}
- ${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'{-open only'} | ${false}
- ${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'close only-}'} | ${false}
- `('$description', ({ inputRegex, input, matches }) => {
- it(`${matches ? 'matches' : 'does not match'}: "${input}"`, () => {
- const match = new RegExp(inputRegex).test(input);
+ let tiptapEditor;
+ let doc;
+ let p;
+ let inlineDiff;
- expect(match).toBe(matches);
- });
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({ extensions: [InlineDiff] });
+ ({
+ builders: { doc, p, inlineDiff },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ inlineDiff: { markType: InlineDiff.name },
+ },
+ }));
+ });
+
+ it.each`
+ input | insertedNode
+ ${'hello{+world+}'} | ${() => p('hello', inlineDiff('world'))}
+ ${'hello{+ world +}'} | ${() => p('hello', inlineDiff(' world '))}
+ ${'{+hello with \nnewline+}'} | ${() => p('{+hello with newline+}')}
+ ${'{+open only'} | ${() => p('{+open only')}
+ ${'close only+}'} | ${() => p('close only+}')}
+ ${'hello{-world-}'} | ${() => p('hello', inlineDiff({ type: 'deletion' }, 'world'))}
+ ${'hello{- world -}'} | ${() => p('hello', inlineDiff({ type: 'deletion' }, ' world '))}
+ ${'hello {- world-}'} | ${() => p('hello ', inlineDiff({ type: 'deletion' }, ' world'))}
+ ${'{-hello world -}'} | ${() => p(inlineDiff({ type: 'deletion' }, 'hello world '))}
+ ${'{-hello with \nnewline-}'} | ${() => p('{-hello with newline-}')}
+ ${'{-open only'} | ${() => p('{-open only')}
+ ${'close only-}'} | ${() => p('close only-}')}
+ `('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => {
+ const expectedDoc = doc(insertedNode());
+
+ triggerMarkInputRule({ tiptapEditor, inputRuleText: input });
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
});
});
diff --git a/spec/frontend/content_editor/extensions/link_spec.js b/spec/frontend/content_editor/extensions/link_spec.js
index 026b2a06df3..ead898554d1 100644
--- a/spec/frontend/content_editor/extensions/link_spec.js
+++ b/spec/frontend/content_editor/extensions/link_spec.js
@@ -1,61 +1,46 @@
-import {
- markdownLinkSyntaxInputRuleRegExp,
- urlSyntaxRegExp,
- extractHrefFromMarkdownLink,
-} from '~/content_editor/extensions/link';
+import Link from '~/content_editor/extensions/link';
+import { createTestEditor, createDocBuilder, triggerMarkInputRule } from '../test_utils';
describe('content_editor/extensions/link', () => {
- describe.each`
- input | matches
- ${'[gitlab](https://gitlab.com)'} | ${true}
- ${'[documentation](readme.md)'} | ${true}
- ${'[link 123](readme.md)'} | ${true}
- ${'[link 123](read me.md)'} | ${true}
- ${'text'} | ${false}
- ${'documentation](readme.md'} | ${false}
- ${'https://www.google.com'} | ${false}
- `('markdownLinkSyntaxInputRuleRegExp', ({ input, matches }) => {
- it(`${matches ? 'matches' : 'does not match'} ${input}`, () => {
- const match = new RegExp(markdownLinkSyntaxInputRuleRegExp).exec(input);
-
- expect(Boolean(match?.groups.href)).toBe(matches);
- });
+ let tiptapEditor;
+ let doc;
+ let p;
+ let link;
+
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({ extensions: [Link] });
+ ({
+ builders: { doc, p, link },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ link: { markType: Link.name },
+ },
+ }));
});
- describe.each`
- input | matches
- ${'http://example.com '} | ${true}
- ${'https://example.com '} | ${true}
- ${'www.example.com '} | ${true}
- ${'example.com/ab.html '} | ${false}
- ${'text'} | ${false}
- ${' http://example.com '} | ${true}
- ${'https://www.google.com '} | ${true}
- `('urlSyntaxRegExp', ({ input, matches }) => {
- it(`${matches ? 'matches' : 'does not match'} ${input}`, () => {
- const match = new RegExp(urlSyntaxRegExp).exec(input);
-
- expect(Boolean(match?.groups.href)).toBe(matches);
- });
+ afterEach(() => {
+ tiptapEditor.destroy();
});
- describe('extractHrefFromMarkdownLink', () => {
- const input = '[gitlab](https://gitlab.com)';
- const href = 'https://gitlab.com';
- let match;
- let result;
-
- beforeEach(() => {
- match = new RegExp(markdownLinkSyntaxInputRuleRegExp).exec(input);
- result = extractHrefFromMarkdownLink(match);
- });
-
- it('extracts the url from a markdown link captured by markdownLinkSyntaxInputRuleRegExp', () => {
- expect(result).toEqual({ href });
- });
-
- it('makes sure that url text is the last capture group', () => {
- expect(match[match.length - 1]).toEqual('gitlab');
- });
+ it.each`
+ input | insertedNode
+ ${'[gitlab](https://gitlab.com)'} | ${() => p(link({ href: 'https://gitlab.com' }, 'gitlab'))}
+ ${'[documentation](readme.md)'} | ${() => p(link({ href: 'readme.md' }, 'documentation'))}
+ ${'[link 123](readme.md)'} | ${() => p(link({ href: 'readme.md' }, 'link 123'))}
+ ${'[link 123](read me.md)'} | ${() => p(link({ href: 'read me.md' }, 'link 123'))}
+ ${'text'} | ${() => p('text')}
+ ${'documentation](readme.md'} | ${() => p('documentation](readme.md')}
+ ${'http://example.com '} | ${() => p(link({ href: 'http://example.com' }, 'http://example.com'))}
+ ${'https://example.com '} | ${() => p(link({ href: 'https://example.com' }, 'https://example.com'))}
+ ${'www.example.com '} | ${() => p(link({ href: 'www.example.com' }, 'www.example.com'))}
+ ${'example.com/ab.html '} | ${() => p('example.com/ab.html')}
+ ${'https://www.google.com '} | ${() => p(link({ href: 'https://www.google.com' }, 'https://www.google.com'))}
+ `('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => {
+ const expectedDoc = doc(insertedNode());
+
+ triggerMarkInputRule({ tiptapEditor, inputRuleText: input });
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
});
});
diff --git a/spec/frontend/content_editor/extensions/math_inline_spec.js b/spec/frontend/content_editor/extensions/math_inline_spec.js
index 82eb85477de..abf10317b5a 100644
--- a/spec/frontend/content_editor/extensions/math_inline_spec.js
+++ b/spec/frontend/content_editor/extensions/math_inline_spec.js
@@ -1,5 +1,5 @@
import MathInline from '~/content_editor/extensions/math_inline';
-import { createTestEditor, createDocBuilder } from '../test_utils';
+import { createTestEditor, createDocBuilder, triggerMarkInputRule } from '../test_utils';
describe('content_editor/extensions/math_inline', () => {
let tiptapEditor;
@@ -26,16 +26,9 @@ describe('content_editor/extensions/math_inline', () => {
${'$`a^2`'} | ${() => p('$`a^2`')}
${'`a^2`$'} | ${() => p('`a^2`$')}
`('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => {
- const { view } = tiptapEditor;
const expectedDoc = doc(insertedNode());
- tiptapEditor.chain().setContent(input).setTextSelection(0).run();
-
- const { state } = tiptapEditor;
- const { selection } = state;
-
- // Triggers the event handler that input rules listen to
- view.someProp('handleTextInput', (f) => f(view, selection.from, input.length + 1, input));
+ triggerMarkInputRule({ tiptapEditor, inputRuleText: input });
expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
});
diff --git a/spec/frontend/content_editor/extensions/table_of_contents_spec.js b/spec/frontend/content_editor/extensions/table_of_contents_spec.js
index 83818899c17..0ddd88b39fe 100644
--- a/spec/frontend/content_editor/extensions/table_of_contents_spec.js
+++ b/spec/frontend/content_editor/extensions/table_of_contents_spec.js
@@ -1,13 +1,17 @@
import TableOfContents from '~/content_editor/extensions/table_of_contents';
-import { createTestEditor, createDocBuilder } from '../test_utils';
+import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils';
-describe('content_editor/extensions/emoji', () => {
+describe('content_editor/extensions/table_of_contents', () => {
let tiptapEditor;
- let builders;
+ let doc;
+ let tableOfContents;
+ let p;
beforeEach(() => {
tiptapEditor = createTestEditor({ extensions: [TableOfContents] });
- ({ builders } = createDocBuilder({
+ ({
+ builders: { doc, p, tableOfContents },
+ } = createDocBuilder({
tiptapEditor,
names: { tableOfContents: { nodeType: TableOfContents.name } },
}));
@@ -15,20 +19,16 @@ describe('content_editor/extensions/emoji', () => {
it.each`
input | insertedNode
- ${'[[_TOC_]]'} | ${'tableOfContents'}
- ${'[TOC]'} | ${'tableOfContents'}
- ${'[toc]'} | ${'p'}
- ${'TOC'} | ${'p'}
- ${'[_TOC_]'} | ${'p'}
- ${'[[TOC]]'} | ${'p'}
+ ${'[[_TOC_]]'} | ${() => tableOfContents()}
+ ${'[TOC]'} | ${() => tableOfContents()}
+ ${'[toc]'} | ${() => p()}
+ ${'TOC'} | ${() => p()}
+ ${'[_TOC_]'} | ${() => p()}
+ ${'[[TOC]]'} | ${() => p()}
`('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => {
- const { doc } = builders;
- const { view } = tiptapEditor;
- const { selection } = view.state;
- const expectedDoc = doc(builders[insertedNode]());
+ const expectedDoc = doc(insertedNode());
- // Triggers the event handler that input rules listen to
- view.someProp('handleTextInput', (f) => f(view, selection.from, selection.to, input));
+ triggerNodeInputRule({ tiptapEditor, inputRuleText: input });
expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
});
diff --git a/spec/frontend/content_editor/extensions/table_spec.js b/spec/frontend/content_editor/extensions/table_spec.js
new file mode 100644
index 00000000000..121fe9192db
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/table_spec.js
@@ -0,0 +1,102 @@
+import Bold from '~/content_editor/extensions/bold';
+import BulletList from '~/content_editor/extensions/bullet_list';
+import ListItem from '~/content_editor/extensions/list_item';
+import Table from '~/content_editor/extensions/table';
+import TableCell from '~/content_editor/extensions/table_cell';
+import TableRow from '~/content_editor/extensions/table_row';
+import TableHeader from '~/content_editor/extensions/table_header';
+import { createTestEditor, createDocBuilder } from '../test_utils';
+
+describe('content_editor/extensions/table', () => {
+ let tiptapEditor;
+ let doc;
+ let p;
+ let table;
+ let tableHeader;
+ let tableCell;
+ let tableRow;
+ let initialDoc;
+ let mockAlert;
+
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({
+ extensions: [Table, TableCell, TableRow, TableHeader, BulletList, Bold, ListItem],
+ });
+
+ ({
+ builders: { doc, p, table, tableCell, tableHeader, tableRow },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ bold: { markType: Bold.name },
+ table: { nodeType: Table.name },
+ tableHeader: { nodeType: TableHeader.name },
+ tableCell: { nodeType: TableCell.name },
+ tableRow: { nodeType: TableRow.name },
+ bulletList: { nodeType: BulletList.name },
+ listItem: { nodeType: ListItem.name },
+ },
+ }));
+
+ initialDoc = doc(
+ table(
+ { isMarkdown: true },
+ tableRow(tableHeader(p('This is')), tableHeader(p('a table'))),
+ tableRow(tableCell(p('this is')), tableCell(p('the first row'))),
+ ),
+ );
+
+ mockAlert = jest.fn();
+ });
+
+ it('triggers a warning (just once) if the table is markdown, but the changes in the document will render an HTML table instead', () => {
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+
+ tiptapEditor.on('alert', mockAlert);
+
+ tiptapEditor.commands.setTextSelection({ from: 20, to: 22 });
+ tiptapEditor.commands.toggleBulletList();
+
+ jest.advanceTimersByTime(1001);
+ expect(mockAlert).toHaveBeenCalled();
+
+ mockAlert.mockReset();
+
+ tiptapEditor.commands.setTextSelection({ from: 4, to: 6 });
+ tiptapEditor.commands.toggleBulletList();
+
+ jest.advanceTimersByTime(1001);
+ expect(mockAlert).not.toHaveBeenCalled();
+ });
+
+ it('does not trigger a warning if the table is markdown, and the changes in the document can generate a markdown table', () => {
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+
+ tiptapEditor.on('alert', mockAlert);
+
+ tiptapEditor.commands.setTextSelection({ from: 20, to: 22 });
+ tiptapEditor.commands.toggleBold();
+
+ jest.advanceTimersByTime(1001);
+ expect(mockAlert).not.toHaveBeenCalled();
+ });
+
+ it('does not trigger any warnings if the table is not markdown', () => {
+ initialDoc = doc(
+ table(
+ tableRow(tableHeader(p('This is')), tableHeader(p('a table'))),
+ tableRow(tableCell(p('this is')), tableCell(p('the first row'))),
+ ),
+ );
+
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+
+ tiptapEditor.on('alert', mockAlert);
+
+ tiptapEditor.commands.setTextSelection({ from: 20, to: 22 });
+ tiptapEditor.commands.toggleBulletList();
+
+ jest.advanceTimersByTime(1001);
+ expect(mockAlert).not.toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/content_editor/extensions/word_break_spec.js b/spec/frontend/content_editor/extensions/word_break_spec.js
new file mode 100644
index 00000000000..23167269d7d
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/word_break_spec.js
@@ -0,0 +1,35 @@
+import WordBreak from '~/content_editor/extensions/word_break';
+import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils';
+
+describe('content_editor/extensions/word_break', () => {
+ let tiptapEditor;
+ let doc;
+ let p;
+ let wordBreak;
+
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({ extensions: [WordBreak] });
+
+ ({
+ builders: { doc, p, wordBreak },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ wordBreak: { nodeType: WordBreak.name },
+ },
+ }));
+ });
+
+ it.each`
+ input | insertedNode
+ ${'<wbr>'} | ${() => p(wordBreak())}
+ ${'<wbr'} | ${() => p()}
+ ${'wbr>'} | ${() => p()}
+ `('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => {
+ const expectedDoc = doc(insertedNode());
+
+ triggerNodeInputRule({ tiptapEditor, inputRuleText: input });
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ });
+});
diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js
index 33056ab9e4a..cfd93c2df10 100644
--- a/spec/frontend/content_editor/services/markdown_serializer_spec.js
+++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js
@@ -34,10 +34,6 @@ import { createTestEditor, createDocBuilder } from '../test_utils';
jest.mock('~/emoji');
-jest.mock('~/content_editor/services/feature_flags', () => ({
- isBlockTablesFeatureEnabled: jest.fn().mockReturnValue(true),
-}));
-
const tiptapEditor = createTestEditor({
extensions: [
Blockquote,
diff --git a/spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js b/spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js
index afe09a75f16..459780cc7cf 100644
--- a/spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js
+++ b/spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js
@@ -10,7 +10,7 @@ import Heading from '~/content_editor/extensions/heading';
import ListItem from '~/content_editor/extensions/list_item';
import trackInputRulesAndShortcuts from '~/content_editor/services/track_input_rules_and_shortcuts';
import { ENTER_KEY, BACKSPACE_KEY } from '~/lib/utils/keys';
-import { createTestEditor } from '../test_utils';
+import { createTestEditor, triggerNodeInputRule } from '../test_utils';
describe('content_editor/services/track_input_rules_and_shortcuts', () => {
let trackingSpy;
@@ -70,14 +70,7 @@ describe('content_editor/services/track_input_rules_and_shortcuts', () => {
describe('when creating a heading using an input rule', () => {
it('sends a tracking event indicating that a heading was created using an input rule', async () => {
const nodeName = Heading.name;
- const { view } = editor;
- const { selection } = view.state;
-
- // Triggers the event handler that input rules listen to
- view.someProp('handleTextInput', (f) => f(view, selection.from, selection.to, '## '));
-
- editor.chain().insertContent(HEADING_TEXT).run();
-
+ triggerNodeInputRule({ tiptapEditor: editor, inputRuleText: '## ' });
expect(trackingSpy).toHaveBeenCalledWith(undefined, INPUT_RULE_TRACKING_ACTION, {
label: CONTENT_EDITOR_TRACKING_LABEL,
property: `${nodeName}`,
diff --git a/spec/frontend/content_editor/test_utils.js b/spec/frontend/content_editor/test_utils.js
index cf5aa3f2938..b236c630e13 100644
--- a/spec/frontend/content_editor/test_utils.js
+++ b/spec/frontend/content_editor/test_utils.js
@@ -119,3 +119,26 @@ export const createTestContentEditorExtension = ({ commands = [] } = {}) => {
},
};
};
+
+export const triggerNodeInputRule = ({ tiptapEditor, inputRuleText }) => {
+ const { view } = tiptapEditor;
+ const { state } = tiptapEditor;
+ const { selection } = state;
+
+ // Triggers the event handler that input rules listen to
+ view.someProp('handleTextInput', (f) => f(view, selection.from, selection.to, inputRuleText));
+};
+
+export const triggerMarkInputRule = ({ tiptapEditor, inputRuleText }) => {
+ const { view } = tiptapEditor;
+
+ tiptapEditor.chain().setContent(inputRuleText).setTextSelection(0).run();
+
+ const { state } = tiptapEditor;
+ const { selection } = state;
+
+ // Triggers the event handler that input rules listen to
+ view.someProp('handleTextInput', (f) =>
+ f(view, selection.from, inputRuleText.length + 1, inputRuleText),
+ );
+};
diff --git a/spec/frontend/create_merge_request_dropdown_spec.js b/spec/frontend/create_merge_request_dropdown_spec.js
index 8878891701f..9f07eea433a 100644
--- a/spec/frontend/create_merge_request_dropdown_spec.js
+++ b/spec/frontend/create_merge_request_dropdown_spec.js
@@ -46,7 +46,10 @@ describe('CreateMergeRequestDropdown', () => {
dropdown
.getRef('contains#hash')
.then(() => {
- expect(axios.get).toHaveBeenCalledWith(endpoint);
+ expect(axios.get).toHaveBeenCalledWith(
+ endpoint,
+ expect.objectContaining({ cancelToken: expect.anything() }),
+ );
})
.then(done)
.catch(done.fail);
diff --git a/spec/frontend/crm/contacts_root_spec.js b/spec/frontend/crm/contacts_root_spec.js
new file mode 100644
index 00000000000..79b85969eb4
--- /dev/null
+++ b/spec/frontend/crm/contacts_root_spec.js
@@ -0,0 +1,60 @@
+import { GlLoadingIcon } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
+import ContactsRoot from '~/crm/components/contacts_root.vue';
+import getGroupContactsQuery from '~/crm/components/queries/get_group_contacts.query.graphql';
+import { getGroupContactsQueryResponse } from './mock_data';
+
+jest.mock('~/flash');
+
+describe('Customer relations contacts root app', () => {
+ Vue.use(VueApollo);
+ let wrapper;
+ let fakeApollo;
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findRowByName = (rowName) => wrapper.findAllByRole('row', { name: rowName });
+ const successQueryHandler = jest.fn().mockResolvedValue(getGroupContactsQueryResponse);
+
+ const mountComponent = ({
+ queryHandler = successQueryHandler,
+ mountFunction = shallowMountExtended,
+ } = {}) => {
+ fakeApollo = createMockApollo([[getGroupContactsQuery, queryHandler]]);
+ wrapper = mountFunction(ContactsRoot, {
+ provide: { groupFullPath: 'flightjs' },
+ apolloProvider: fakeApollo,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ fakeApollo = null;
+ });
+
+ it('should render loading spinner', () => {
+ mountComponent();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ it('should render error message on reject', async () => {
+ mountComponent({ queryHandler: jest.fn().mockRejectedValue('ERROR') });
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalled();
+ });
+
+ it('renders correct results', async () => {
+ mountComponent({ mountFunction: mountExtended });
+ await waitForPromises();
+
+ expect(findRowByName(/Marty/i)).toHaveLength(1);
+ expect(findRowByName(/George/i)).toHaveLength(1);
+ expect(findRowByName(/jd@gitlab.com/i)).toHaveLength(1);
+ });
+});
diff --git a/spec/frontend/crm/mock_data.js b/spec/frontend/crm/mock_data.js
new file mode 100644
index 00000000000..4197621aaa6
--- /dev/null
+++ b/spec/frontend/crm/mock_data.js
@@ -0,0 +1,81 @@
+export const getGroupContactsQueryResponse = {
+ data: {
+ group: {
+ __typename: 'Group',
+ id: 'gid://gitlab/Group/26',
+ contacts: {
+ nodes: [
+ {
+ __typename: 'CustomerRelationsContact',
+ id: 'gid://gitlab/CustomerRelations::Contact/12',
+ firstName: 'Marty',
+ lastName: 'McFly',
+ email: 'example@gitlab.com',
+ phone: null,
+ description: null,
+ organization: {
+ __typename: 'CustomerRelationsOrganization',
+ id: 'gid://gitlab/CustomerRelations::Organization/2',
+ name: 'Tech Giant Inc',
+ },
+ },
+ {
+ __typename: 'CustomerRelationsContact',
+ id: 'gid://gitlab/CustomerRelations::Contact/16',
+ firstName: 'Boy',
+ lastName: 'George',
+ email: null,
+ phone: null,
+ description: null,
+ organization: null,
+ },
+ {
+ __typename: 'CustomerRelationsContact',
+ id: 'gid://gitlab/CustomerRelations::Contact/13',
+ firstName: 'Jane',
+ lastName: 'Doe',
+ email: 'jd@gitlab.com',
+ phone: '+44 44 4444 4444',
+ description: 'Vice President',
+ organization: null,
+ },
+ ],
+ __typename: 'CustomerRelationsContactConnection',
+ },
+ },
+ },
+};
+
+export const getGroupOrganizationsQueryResponse = {
+ data: {
+ group: {
+ __typename: 'Group',
+ id: 'gid://gitlab/Group/26',
+ organizations: {
+ nodes: [
+ {
+ __typename: 'CustomerRelationsOrganization',
+ id: 'gid://gitlab/CustomerRelations::Organization/1',
+ name: 'Test Inc',
+ defaultRate: 100,
+ description: null,
+ },
+ {
+ __typename: 'CustomerRelationsOrganization',
+ id: 'gid://gitlab/CustomerRelations::Organization/2',
+ name: 'ABC Company',
+ defaultRate: 110,
+ description: 'VIP',
+ },
+ {
+ __typename: 'CustomerRelationsOrganization',
+ id: 'gid://gitlab/CustomerRelations::Organization/3',
+ name: 'GitLab',
+ defaultRate: 120,
+ description: null,
+ },
+ ],
+ },
+ },
+ },
+};
diff --git a/spec/frontend/crm/organizations_root_spec.js b/spec/frontend/crm/organizations_root_spec.js
new file mode 100644
index 00000000000..a69a099e03d
--- /dev/null
+++ b/spec/frontend/crm/organizations_root_spec.js
@@ -0,0 +1,60 @@
+import { GlLoadingIcon } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
+import OrganizationsRoot from '~/crm/components/organizations_root.vue';
+import getGroupOrganizationsQuery from '~/crm/components/queries/get_group_organizations.query.graphql';
+import { getGroupOrganizationsQueryResponse } from './mock_data';
+
+jest.mock('~/flash');
+
+describe('Customer relations organizations root app', () => {
+ Vue.use(VueApollo);
+ let wrapper;
+ let fakeApollo;
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findRowByName = (rowName) => wrapper.findAllByRole('row', { name: rowName });
+ const successQueryHandler = jest.fn().mockResolvedValue(getGroupOrganizationsQueryResponse);
+
+ const mountComponent = ({
+ queryHandler = successQueryHandler,
+ mountFunction = shallowMountExtended,
+ } = {}) => {
+ fakeApollo = createMockApollo([[getGroupOrganizationsQuery, queryHandler]]);
+ wrapper = mountFunction(OrganizationsRoot, {
+ provide: { groupFullPath: 'flightjs' },
+ apolloProvider: fakeApollo,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ fakeApollo = null;
+ });
+
+ it('should render loading spinner', () => {
+ mountComponent();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ it('should render error message on reject', async () => {
+ mountComponent({ queryHandler: jest.fn().mockRejectedValue('ERROR') });
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalled();
+ });
+
+ it('renders correct results', async () => {
+ mountComponent({ mountFunction: mountExtended });
+ await waitForPromises();
+
+ expect(findRowByName(/Test Inc/i)).toHaveLength(1);
+ expect(findRowByName(/VIP/i)).toHaveLength(1);
+ expect(findRowByName(/120/i)).toHaveLength(1);
+ });
+});
diff --git a/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js b/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js
index c41adf523f8..2001f5c1441 100644
--- a/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js
+++ b/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js
@@ -4,8 +4,6 @@ import { TEST_HOST } from 'helpers/test_constants';
import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
import axios from '~/lib/utils/axios_utils';
-const { CancelToken } = axios;
-
describe('custom metrics form fields component', () => {
let wrapper;
let mockAxios;
@@ -116,14 +114,14 @@ describe('custom metrics form fields component', () => {
it('receives and validates a persisted value', () => {
const query = 'persistedQuery';
- const axiosPost = jest.spyOn(axios, 'post');
- const source = CancelToken.source();
+ jest.spyOn(axios, 'post');
+
mountComponent({ metricPersisted: true, ...makeFormData({ query }) });
- expect(axiosPost).toHaveBeenCalledWith(
+ expect(axios.post).toHaveBeenCalledWith(
validateQueryPath,
{ query },
- { cancelToken: source.token },
+ expect.objectContaining({ cancelToken: expect.anything() }),
);
expect(getNamedInput(queryInputName).value).toBe(query);
jest.runAllTimers();
diff --git a/spec/frontend/cycle_analytics/metric_popover_spec.js b/spec/frontend/cycle_analytics/metric_popover_spec.js
new file mode 100644
index 00000000000..5a622fcacd5
--- /dev/null
+++ b/spec/frontend/cycle_analytics/metric_popover_spec.js
@@ -0,0 +1,102 @@
+import { GlLink, GlIcon } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import MetricPopover from '~/cycle_analytics/components/metric_popover.vue';
+
+const MOCK_METRIC = {
+ key: 'deployment-frequency',
+ label: 'Deployment Frequency',
+ value: '10.0',
+ unit: 'per day',
+ description: 'Average number of deployments to production per day.',
+ links: [],
+};
+
+describe('MetricPopover', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ return shallowMountExtended(MetricPopover, {
+ propsData: {
+ target: 'deployment-frequency',
+ ...props,
+ },
+ stubs: {
+ 'gl-popover': { template: '<div><slot name="title"></slot><slot></slot></div>' },
+ },
+ });
+ };
+
+ const findMetricLabel = () => wrapper.findByTestId('metric-label');
+ const findAllMetricLinks = () => wrapper.findAll('[data-testid="metric-link"]');
+ const findMetricDescription = () => wrapper.findByTestId('metric-description');
+ const findMetricDocsLink = () => wrapper.findByTestId('metric-docs-link');
+ const findMetricDocsLinkIcon = () => findMetricDocsLink().find(GlIcon);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the metric label', () => {
+ wrapper = createComponent({ metric: MOCK_METRIC });
+ expect(findMetricLabel().text()).toBe(MOCK_METRIC.label);
+ });
+
+ it('renders the metric description', () => {
+ wrapper = createComponent({ metric: MOCK_METRIC });
+ expect(findMetricDescription().text()).toBe(MOCK_METRIC.description);
+ });
+
+ describe('with links', () => {
+ const links = [
+ {
+ name: 'Deployment frequency',
+ url: '/groups/gitlab-org/-/analytics/ci_cd?tab=deployment-frequency',
+ label: 'Dashboard',
+ },
+ {
+ name: 'Another link',
+ url: '/groups/gitlab-org/-/analytics/another-link',
+ label: 'Another link',
+ },
+ ];
+ const docsLink = {
+ name: 'Deployment frequency',
+ url: '/help/user/analytics/index#definitions',
+ label: 'Go to docs',
+ docs_link: true,
+ };
+ const linksWithDocs = [...links, docsLink];
+
+ describe.each`
+ hasDocsLink | allLinks | displayedMetricLinks
+ ${true} | ${linksWithDocs} | ${links}
+ ${false} | ${links} | ${links}
+ `(
+ 'when one link has docs_link=$hasDocsLink',
+ ({ hasDocsLink, allLinks, displayedMetricLinks }) => {
+ beforeEach(() => {
+ wrapper = createComponent({ metric: { ...MOCK_METRIC, links: allLinks } });
+ });
+
+ displayedMetricLinks.forEach((link, idx) => {
+ it(`renders a link for "${link.name}"`, () => {
+ const allLinkContainers = findAllMetricLinks();
+
+ expect(allLinkContainers.at(idx).text()).toContain(link.name);
+ expect(allLinkContainers.at(idx).find(GlLink).attributes('href')).toBe(link.url);
+ });
+ });
+
+ it(`${hasDocsLink ? 'renders' : "doesn't render"} a docs link`, () => {
+ expect(findMetricDocsLink().exists()).toBe(hasDocsLink);
+
+ if (hasDocsLink) {
+ expect(findMetricDocsLink().attributes('href')).toBe(docsLink.url);
+ expect(findMetricDocsLink().text()).toBe(docsLink.label);
+ expect(findMetricDocsLinkIcon().attributes('name')).toBe('external-link');
+ }
+ });
+ },
+ );
+ });
+});
diff --git a/spec/frontend/cycle_analytics/mock_data.js b/spec/frontend/cycle_analytics/mock_data.js
index 1882457960a..c482bd4e910 100644
--- a/spec/frontend/cycle_analytics/mock_data.js
+++ b/spec/frontend/cycle_analytics/mock_data.js
@@ -1,10 +1,14 @@
-/* eslint-disable import/no-deprecated */
+import valueStreamAnalyticsStages from 'test_fixtures/projects/analytics/value_stream_analytics/stages.json';
+import issueStageFixtures from 'test_fixtures/projects/analytics/value_stream_analytics/events/issue.json';
+import planStageFixtures from 'test_fixtures/projects/analytics/value_stream_analytics/events/plan.json';
+import reviewStageFixtures from 'test_fixtures/projects/analytics/value_stream_analytics/events/review.json';
+import codeStageFixtures from 'test_fixtures/projects/analytics/value_stream_analytics/events/code.json';
+import testStageFixtures from 'test_fixtures/projects/analytics/value_stream_analytics/events/test.json';
+import stagingStageFixtures from 'test_fixtures/projects/analytics/value_stream_analytics/events/staging.json';
-import { getJSONFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import {
DEFAULT_VALUE_STREAM,
- DEFAULT_DAYS_IN_PAST,
PAGINATION_TYPE,
PAGINATION_SORT_DIRECTION_DESC,
PAGINATION_SORT_FIELD_END_EVENT,
@@ -12,6 +16,7 @@ import {
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { getDateInPast } from '~/lib/utils/datetime_utility';
+const DEFAULT_DAYS_IN_PAST = 30;
export const createdBefore = new Date(2019, 0, 14);
export const createdAfter = getDateInPast(createdBefore, DEFAULT_DAYS_IN_PAST);
@@ -20,28 +25,16 @@ export const deepCamelCase = (obj) => convertObjectPropsToCamelCase(obj, { deep:
export const getStageByTitle = (stages, title) =>
stages.find((stage) => stage.title && stage.title.toLowerCase().trim() === title) || {};
-const fixtureEndpoints = {
- customizableCycleAnalyticsStagesAndEvents:
- 'projects/analytics/value_stream_analytics/stages.json',
- stageEvents: (stage) => `projects/analytics/value_stream_analytics/events/${stage}.json`,
- metricsData: 'projects/analytics/value_stream_analytics/summary.json',
-};
-
-export const metricsData = getJSONFixture(fixtureEndpoints.metricsData);
-
-export const customizableStagesAndEvents = getJSONFixture(
- fixtureEndpoints.customizableCycleAnalyticsStagesAndEvents,
-);
-
export const defaultStages = ['issue', 'plan', 'review', 'code', 'test', 'staging'];
-const stageFixtures = defaultStages.reduce((acc, stage) => {
- const events = getJSONFixture(fixtureEndpoints.stageEvents(stage));
- return {
- ...acc,
- [stage]: events,
- };
-}, {});
+const stageFixtures = {
+ issue: issueStageFixtures,
+ plan: planStageFixtures,
+ review: reviewStageFixtures,
+ code: codeStageFixtures,
+ test: testStageFixtures,
+ staging: stagingStageFixtures,
+};
export const summary = [
{ value: '20', title: 'New Issues' },
@@ -260,7 +253,7 @@ export const selectedProjects = [
},
];
-export const rawValueStreamStages = customizableStagesAndEvents.stages;
+export const rawValueStreamStages = valueStreamAnalyticsStages.stages;
export const valueStreamStages = rawValueStreamStages.map((s) =>
convertObjectPropsToCamelCase(s, { deep: true }),
diff --git a/spec/frontend/cycle_analytics/store/actions_spec.js b/spec/frontend/cycle_analytics/store/actions_spec.js
index 993e6b6b73a..e775e941b4c 100644
--- a/spec/frontend/cycle_analytics/store/actions_spec.js
+++ b/spec/frontend/cycle_analytics/store/actions_spec.js
@@ -57,22 +57,12 @@ describe('Project Value Stream Analytics actions', () => {
const mutationTypes = (arr) => arr.map(({ type }) => type);
- const mockFetchStageDataActions = [
- { type: 'setLoading', payload: true },
- { type: 'fetchCycleAnalyticsData' },
- { type: 'fetchStageData' },
- { type: 'fetchStageMedians' },
- { type: 'fetchStageCountValues' },
- { type: 'setLoading', payload: false },
- ];
-
describe.each`
- action | payload | expectedActions | expectedMutations
- ${'setLoading'} | ${true} | ${[]} | ${[{ type: 'SET_LOADING', payload: true }]}
- ${'setDateRange'} | ${{ createdAfter, createdBefore }} | ${mockFetchStageDataActions} | ${[mockSetDateActionCommit]}
- ${'setFilters'} | ${[]} | ${mockFetchStageDataActions} | ${[]}
- ${'setSelectedStage'} | ${{ selectedStage }} | ${[{ type: 'fetchStageData' }]} | ${[{ type: 'SET_SELECTED_STAGE', payload: { selectedStage } }]}
- ${'setSelectedValueStream'} | ${{ selectedValueStream }} | ${[{ type: 'fetchValueStreamStages' }, { type: 'fetchCycleAnalyticsData' }]} | ${[{ type: 'SET_SELECTED_VALUE_STREAM', payload: { selectedValueStream } }]}
+ action | payload | expectedActions | expectedMutations
+ ${'setDateRange'} | ${{ createdAfter, createdBefore }} | ${[{ type: 'refetchStageData' }]} | ${[mockSetDateActionCommit]}
+ ${'setFilters'} | ${[]} | ${[{ type: 'refetchStageData' }]} | ${[]}
+ ${'setSelectedStage'} | ${{ selectedStage }} | ${[{ type: 'refetchStageData' }]} | ${[{ type: 'SET_SELECTED_STAGE', payload: { selectedStage } }]}
+ ${'setSelectedValueStream'} | ${{ selectedValueStream }} | ${[{ type: 'fetchValueStreamStages' }]} | ${[{ type: 'SET_SELECTED_VALUE_STREAM', payload: { selectedValueStream } }]}
`('$action', ({ action, payload, expectedActions, expectedMutations }) => {
const types = mutationTypes(expectedMutations);
it(`will dispatch ${expectedActions} and commit ${types}`, () =>
@@ -86,9 +76,18 @@ describe('Project Value Stream Analytics actions', () => {
});
describe('initializeVsa', () => {
- let mockDispatch;
- let mockCommit;
- const payload = { endpoints: mockEndpoints };
+ const selectedAuthor = 'Author';
+ const selectedMilestone = 'Milestone 1';
+ const selectedAssigneeList = ['Assignee 1', 'Assignee 2'];
+ const selectedLabelList = ['Label 1', 'Label 2'];
+ const payload = {
+ endpoints: mockEndpoints,
+ selectedAuthor,
+ selectedMilestone,
+ selectedAssigneeList,
+ selectedLabelList,
+ selectedStage,
+ };
const mockFilterEndpoints = {
groupEndpoint: 'foo',
labelsEndpoint: mockLabelsPath,
@@ -96,27 +95,63 @@ describe('Project Value Stream Analytics actions', () => {
projectEndpoint: '/namespace/-/analytics/value_stream_analytics/value_streams',
};
+ it('will dispatch fetchValueStreams actions and commit SET_LOADING and INITIALIZE_VSA', () => {
+ return testAction({
+ action: actions.initializeVsa,
+ state: {},
+ payload,
+ expectedMutations: [
+ { type: 'INITIALIZE_VSA', payload },
+ { type: 'SET_LOADING', payload: true },
+ { type: 'SET_LOADING', payload: false },
+ ],
+ expectedActions: [
+ { type: 'filters/setEndpoints', payload: mockFilterEndpoints },
+ {
+ type: 'filters/initialize',
+ payload: { selectedAuthor, selectedMilestone, selectedAssigneeList, selectedLabelList },
+ },
+ { type: 'fetchValueStreams' },
+ { type: 'setInitialStage', payload: selectedStage },
+ ],
+ });
+ });
+ });
+
+ describe('setInitialStage', () => {
beforeEach(() => {
- mockDispatch = jest.fn(() => Promise.resolve());
- mockCommit = jest.fn();
+ state = { ...state, stages: allowedStages };
});
- it('will dispatch the setLoading and fetchValueStreams actions and commit INITIALIZE_VSA', async () => {
- await actions.initializeVsa(
- {
- ...state,
- dispatch: mockDispatch,
- commit: mockCommit,
- },
- payload,
- );
- expect(mockCommit).toHaveBeenCalledWith('INITIALIZE_VSA', { endpoints: mockEndpoints });
-
- expect(mockDispatch).toHaveBeenCalledTimes(4);
- expect(mockDispatch).toHaveBeenCalledWith('filters/setEndpoints', mockFilterEndpoints);
- expect(mockDispatch).toHaveBeenCalledWith('setLoading', true);
- expect(mockDispatch).toHaveBeenCalledWith('fetchValueStreams');
- expect(mockDispatch).toHaveBeenCalledWith('setLoading', false);
+ describe('with a selected stage', () => {
+ it('will commit `SET_SELECTED_STAGE` and fetchValueStreamStageData actions', () => {
+ const fakeStage = { ...selectedStage, id: 'fake', name: 'fake-stae' };
+ return testAction({
+ action: actions.setInitialStage,
+ state,
+ payload: fakeStage,
+ expectedMutations: [
+ {
+ type: 'SET_SELECTED_STAGE',
+ payload: fakeStage,
+ },
+ ],
+ expectedActions: [{ type: 'fetchValueStreamStageData' }],
+ });
+ });
+ });
+
+ describe('without a selected stage', () => {
+ it('will select the first stage from the value stream', () => {
+ const [firstStage] = allowedStages;
+ testAction({
+ action: actions.setInitialStage,
+ state,
+ payload: null,
+ expectedMutations: [{ type: 'SET_SELECTED_STAGE', payload: firstStage }],
+ expectedActions: [{ type: 'fetchValueStreamStageData' }],
+ });
+ });
});
});
@@ -270,12 +305,7 @@ describe('Project Value Stream Analytics actions', () => {
state,
payload: {},
expectedMutations: [{ type: 'REQUEST_VALUE_STREAMS' }],
- expectedActions: [
- { type: 'receiveValueStreamsSuccess' },
- { type: 'setSelectedStage' },
- { type: 'fetchStageMedians' },
- { type: 'fetchStageCountValues' },
- ],
+ expectedActions: [{ type: 'receiveValueStreamsSuccess' }],
}));
describe('with a failing request', () => {
@@ -483,4 +513,34 @@ describe('Project Value Stream Analytics actions', () => {
}));
});
});
+
+ describe('refetchStageData', () => {
+ it('will commit SET_LOADING and dispatch fetchValueStreamStageData actions', () =>
+ testAction({
+ action: actions.refetchStageData,
+ state,
+ payload: {},
+ expectedMutations: [
+ { type: 'SET_LOADING', payload: true },
+ { type: 'SET_LOADING', payload: false },
+ ],
+ expectedActions: [{ type: 'fetchValueStreamStageData' }],
+ }));
+ });
+
+ describe('fetchValueStreamStageData', () => {
+ it('will dispatch the fetchCycleAnalyticsData, fetchStageData, fetchStageMedians and fetchStageCountValues actions', () =>
+ testAction({
+ action: actions.fetchValueStreamStageData,
+ state,
+ payload: {},
+ expectedMutations: [],
+ expectedActions: [
+ { type: 'fetchCycleAnalyticsData' },
+ { type: 'fetchStageData' },
+ { type: 'fetchStageMedians' },
+ { type: 'fetchStageCountValues' },
+ ],
+ }));
+ });
});
diff --git a/spec/frontend/cycle_analytics/store/mutations_spec.js b/spec/frontend/cycle_analytics/store/mutations_spec.js
index 4860225c995..2670a390e9c 100644
--- a/spec/frontend/cycle_analytics/store/mutations_spec.js
+++ b/spec/frontend/cycle_analytics/store/mutations_spec.js
@@ -101,6 +101,7 @@ describe('Project Value Stream Analytics mutations', () => {
${types.SET_SELECTED_VALUE_STREAM} | ${selectedValueStream} | ${'selectedValueStream'} | ${selectedValueStream}
${types.SET_PAGINATION} | ${pagination} | ${'pagination'} | ${{ ...pagination, sort: PAGINATION_SORT_FIELD_END_EVENT, direction: PAGINATION_SORT_DIRECTION_DESC }}
${types.SET_PAGINATION} | ${{ ...pagination, sort: 'duration', direction: 'asc' }} | ${'pagination'} | ${{ ...pagination, sort: 'duration', direction: 'asc' }}
+ ${types.SET_SELECTED_STAGE} | ${selectedStage} | ${'selectedStage'} | ${selectedStage}
${types.RECEIVE_VALUE_STREAMS_SUCCESS} | ${[selectedValueStream]} | ${'valueStreams'} | ${[selectedValueStream]}
${types.RECEIVE_VALUE_STREAM_STAGES_SUCCESS} | ${{ stages: rawValueStreamStages }} | ${'stages'} | ${valueStreamStages}
${types.RECEIVE_STAGE_MEDIANS_SUCCESS} | ${rawStageMedians} | ${'medians'} | ${formattedStageMedians}
diff --git a/spec/frontend/cycle_analytics/utils_spec.js b/spec/frontend/cycle_analytics/utils_spec.js
index 74d64cd8d71..a6d6d022781 100644
--- a/spec/frontend/cycle_analytics/utils_spec.js
+++ b/spec/frontend/cycle_analytics/utils_spec.js
@@ -1,11 +1,11 @@
-import { useFakeDate } from 'helpers/fake_date';
+import metricsData from 'test_fixtures/projects/analytics/value_stream_analytics/summary.json';
import {
transformStagesForPathNavigation,
medianTimeToParsedSeconds,
formatMedianValues,
filterStagesByHiddenStatus,
- calculateFormattedDayInPast,
prepareTimeMetricsData,
+ buildCycleAnalyticsInitialData,
} from '~/cycle_analytics/utils';
import { slugify } from '~/lib/utils/text_utility';
import {
@@ -14,7 +14,6 @@ import {
stageMedians,
pathNavIssueMetric,
rawStageMedians,
- metricsData,
} from './mock_data';
describe('Value stream analytics utils', () => {
@@ -90,14 +89,6 @@ describe('Value stream analytics utils', () => {
});
});
- describe('calculateFormattedDayInPast', () => {
- useFakeDate(1815, 11, 10);
-
- it('will return 2 dates, now and past', () => {
- expect(calculateFormattedDayInPast(5)).toEqual({ now: '1815-12-10', past: '1815-12-05' });
- });
- });
-
describe('prepareTimeMetricsData', () => {
let prepared;
const [first, second] = metricsData;
@@ -125,4 +116,87 @@ describe('Value stream analytics utils', () => {
]);
});
});
+
+ describe('buildCycleAnalyticsInitialData', () => {
+ let res = null;
+ const projectId = '5';
+ const createdAfter = '2021-09-01';
+ const createdBefore = '2021-11-06';
+ const groupId = '146';
+ const groupPath = 'fake-group';
+ const fullPath = 'fake-group/fake-project';
+ const labelsPath = '/fake-group/fake-project/-/labels.json';
+ const milestonesPath = '/fake-group/fake-project/-/milestones.json';
+ const requestPath = '/fake-group/fake-project/-/value_stream_analytics';
+
+ const rawData = {
+ projectId,
+ createdBefore,
+ createdAfter,
+ fullPath,
+ requestPath,
+ labelsPath,
+ milestonesPath,
+ groupId,
+ groupPath,
+ };
+
+ describe('with minimal data', () => {
+ beforeEach(() => {
+ res = buildCycleAnalyticsInitialData(rawData);
+ });
+
+ it('sets the projectId', () => {
+ expect(res.projectId).toBe(parseInt(projectId, 10));
+ });
+
+ it('sets the date range', () => {
+ expect(res.createdBefore).toEqual(new Date(createdBefore));
+ expect(res.createdAfter).toEqual(new Date(createdAfter));
+ });
+
+ it('sets the endpoints', () => {
+ const { endpoints } = res;
+ expect(endpoints.fullPath).toBe(fullPath);
+ expect(endpoints.requestPath).toBe(requestPath);
+ expect(endpoints.labelsPath).toBe(labelsPath);
+ expect(endpoints.milestonesPath).toBe(milestonesPath);
+ expect(endpoints.groupId).toBe(parseInt(groupId, 10));
+ expect(endpoints.groupPath).toBe(groupPath);
+ });
+
+ it('returns null when there is no stage', () => {
+ expect(res.selectedStage).toBeNull();
+ });
+
+ it('returns false for missing features', () => {
+ expect(res.features.cycleAnalyticsForGroups).toBe(false);
+ });
+ });
+
+ describe('with a stage set', () => {
+ const jsonStage = '{"id":"fakeStage","title":"fakeStage"}';
+
+ it('parses the selectedStage data', () => {
+ res = buildCycleAnalyticsInitialData({ ...rawData, stage: jsonStage });
+
+ const { selectedStage: stage } = res;
+
+ expect(stage.id).toBe('fakeStage');
+ expect(stage.title).toBe('fakeStage');
+ });
+ });
+
+ describe('with features set', () => {
+ const fakeFeatures = { cycleAnalyticsForGroups: true };
+
+ it('sets the feature flags', () => {
+ res = buildCycleAnalyticsInitialData({
+ ...rawData,
+ gon: { licensed_features: fakeFeatures },
+ });
+ expect(res.features).toEqual(fakeFeatures);
+ });
+ });
+ });
});
diff --git a/spec/frontend/cycle_analytics/value_stream_metrics_spec.js b/spec/frontend/cycle_analytics/value_stream_metrics_spec.js
index ffdb49a828c..c97e4845bc2 100644
--- a/spec/frontend/cycle_analytics/value_stream_metrics_spec.js
+++ b/spec/frontend/cycle_analytics/value_stream_metrics_spec.js
@@ -1,13 +1,16 @@
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { shallowMount } from '@vue/test-utils';
+import metricsData from 'test_fixtures/projects/analytics/value_stream_analytics/summary.json';
import waitForPromises from 'helpers/wait_for_promises';
import { METRIC_TYPE_SUMMARY } from '~/api/analytics_api';
import ValueStreamMetrics from '~/cycle_analytics/components/value_stream_metrics.vue';
import createFlash from '~/flash';
-import { group, metricsData } from './mock_data';
+import { redirectTo } from '~/lib/utils/url_utility';
+import { group } from './mock_data';
jest.mock('~/flash');
+jest.mock('~/lib/utils/url_utility');
describe('ValueStreamMetrics', () => {
let wrapper;
@@ -43,7 +46,6 @@ describe('ValueStreamMetrics', () => {
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
describe('with successful requests', () => {
@@ -55,7 +57,23 @@ describe('ValueStreamMetrics', () => {
it('will display a loader with pending requests', async () => {
await wrapper.vm.$nextTick();
- expect(wrapper.find(GlSkeletonLoading).exists()).toBe(true);
+ expect(wrapper.findComponent(GlSkeletonLoading).exists()).toBe(true);
+ });
+
+ it('renders hidden GlSingleStat components for each metric', async () => {
+ await waitForPromises();
+
+ wrapper.setData({ isLoading: true });
+
+ await wrapper.vm.$nextTick();
+
+ const components = findMetrics();
+
+ expect(components).toHaveLength(metricsData.length);
+
+ metricsData.forEach((metric, index) => {
+ expect(components.at(index).isVisible()).toBe(false);
+ });
});
describe('with data loaded', () => {
@@ -67,19 +85,31 @@ describe('ValueStreamMetrics', () => {
expectToHaveRequest({ params: {} });
});
- it.each`
- index | value | title | unit
- ${0} | ${metricsData[0].value} | ${metricsData[0].title} | ${metricsData[0].unit}
- ${1} | ${metricsData[1].value} | ${metricsData[1].title} | ${metricsData[1].unit}
- ${2} | ${metricsData[2].value} | ${metricsData[2].title} | ${metricsData[2].unit}
- ${3} | ${metricsData[3].value} | ${metricsData[3].title} | ${metricsData[3].unit}
- `(
- 'renders a single stat component for the $title with value and unit',
- ({ index, value, title, unit }) => {
+ describe.each`
+ index | value | title | unit | clickable
+ ${0} | ${metricsData[0].value} | ${metricsData[0].title} | ${metricsData[0].unit} | ${false}
+ ${1} | ${metricsData[1].value} | ${metricsData[1].title} | ${metricsData[1].unit} | ${false}
+ ${2} | ${metricsData[2].value} | ${metricsData[2].title} | ${metricsData[2].unit} | ${false}
+ ${3} | ${metricsData[3].value} | ${metricsData[3].title} | ${metricsData[3].unit} | ${true}
+ `('metric tiles', ({ index, value, title, unit, clickable }) => {
+ it(`renders a single stat component for "${title}" with value and unit`, () => {
const metric = findMetrics().at(index);
expect(metric.props()).toMatchObject({ value, title, unit: unit ?? '' });
- },
- );
+ expect(metric.isVisible()).toBe(true);
+ });
+
+ it(`${
+ clickable ? 'redirects' : "doesn't redirect"
+ } when the user clicks the "${title}" metric`, () => {
+ const metric = findMetrics().at(index);
+ metric.vm.$emit('click');
+ if (clickable) {
+ expect(redirectTo).toHaveBeenCalledWith(metricsData[index].links[0].url);
+ } else {
+ expect(redirectTo).not.toHaveBeenCalled();
+ }
+ });
+ });
it('will not display a loading icon', () => {
expect(wrapper.find(GlSkeletonLoading).exists()).toBe(false);
diff --git a/spec/frontend/delete_label_modal_spec.js b/spec/frontend/delete_label_modal_spec.js
index df70d3a8393..0b3e6fe652a 100644
--- a/spec/frontend/delete_label_modal_spec.js
+++ b/spec/frontend/delete_label_modal_spec.js
@@ -40,7 +40,7 @@ describe('DeleteLabelModal', () => {
it('starts with only js-containers', () => {
expect(findJsHooks()).toHaveLength(buttons.length);
- expect(findModal()).not.toExist();
+ expect(findModal()).toBe(null);
});
describe('when first button clicked', () => {
@@ -54,7 +54,7 @@ describe('DeleteLabelModal', () => {
});
it('renders GlModal', () => {
- expect(findModal()).toExist();
+ expect(findModal()).not.toBe(null);
});
});
diff --git a/spec/frontend/deploy_keys/components/key_spec.js b/spec/frontend/deploy_keys/components/key_spec.js
index 511b9d6ef55..51c120d8213 100644
--- a/spec/frontend/deploy_keys/components/key_spec.js
+++ b/spec/frontend/deploy_keys/components/key_spec.js
@@ -50,20 +50,20 @@ describe('Deploy keys key', () => {
it('shows pencil button for editing', () => {
createComponent({ deployKey });
- expect(wrapper.find('.btn [data-testid="pencil-icon"]')).toExist();
+ expect(wrapper.find('.btn [data-testid="pencil-icon"]').exists()).toBe(true);
});
it('shows disable button when the project is not deletable', () => {
createComponent({ deployKey });
- expect(wrapper.find('.btn [data-testid="cancel-icon"]')).toExist();
+ expect(wrapper.find('.btn [data-testid="cancel-icon"]').exists()).toBe(true);
});
it('shows remove button when the project is deletable', () => {
createComponent({
deployKey: { ...deployKey, destroyed_when_orphaned: true, almost_orphaned: true },
});
- expect(wrapper.find('.btn [data-testid="remove-icon"]')).toExist();
+ expect(wrapper.find('.btn [data-testid="remove-icon"]').exists()).toBe(true);
});
});
@@ -137,7 +137,7 @@ describe('Deploy keys key', () => {
it('shows pencil button for editing', () => {
createComponent({ deployKey });
- expect(wrapper.find('.btn [data-testid="pencil-icon"]')).toExist();
+ expect(wrapper.find('.btn [data-testid="pencil-icon"]').exists()).toBe(true);
});
it('shows disable button when key is enabled', () => {
@@ -145,7 +145,7 @@ describe('Deploy keys key', () => {
createComponent({ deployKey });
- expect(wrapper.find('.btn [data-testid="cancel-icon"]')).toExist();
+ expect(wrapper.find('.btn [data-testid="cancel-icon"]').exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/deploy_keys/components/keys_panel_spec.js b/spec/frontend/deploy_keys/components/keys_panel_spec.js
index f3b907e5450..f5f76d5d493 100644
--- a/spec/frontend/deploy_keys/components/keys_panel_spec.js
+++ b/spec/frontend/deploy_keys/components/keys_panel_spec.js
@@ -37,7 +37,7 @@ describe('Deploy keys panel', () => {
mountComponent();
const tableHeader = findTableRowHeader();
- expect(tableHeader).toExist();
+ expect(tableHeader.exists()).toBe(true);
expect(tableHeader.text()).toContain('Deploy key');
expect(tableHeader.text()).toContain('Project usage');
expect(tableHeader.text()).toContain('Created');
diff --git a/spec/frontend/deprecated_jquery_dropdown_spec.js b/spec/frontend/deprecated_jquery_dropdown_spec.js
index 7e4c6e131b4..bec91fe5fc5 100644
--- a/spec/frontend/deprecated_jquery_dropdown_spec.js
+++ b/spec/frontend/deprecated_jquery_dropdown_spec.js
@@ -1,10 +1,9 @@
/* eslint-disable no-param-reassign */
import $ from 'jquery';
+import mockProjects from 'test_fixtures_static/projects.json';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import '~/lib/utils/common_utils';
-// eslint-disable-next-line import/no-deprecated
-import { getJSONFixture } from 'helpers/fixtures';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({
@@ -68,8 +67,7 @@ describe('deprecatedJQueryDropdown', () => {
loadFixtures('static/deprecated_jquery_dropdown.html');
test.dropdownContainerElement = $('.dropdown.inline');
test.$dropdownMenuElement = $('.dropdown-menu', test.dropdownContainerElement);
- // eslint-disable-next-line import/no-deprecated
- test.projectsData = getJSONFixture('static/projects.json');
+ test.projectsData = JSON.parse(JSON.stringify(mockProjects));
});
afterEach(() => {
diff --git a/spec/frontend/design_management/components/list/item_spec.js b/spec/frontend/design_management/components/list/item_spec.js
index 58636ece91e..ed105b112be 100644
--- a/spec/frontend/design_management/components/list/item_spec.js
+++ b/spec/frontend/design_management/components/list/item_spec.js
@@ -87,7 +87,7 @@ describe('Design management list item component', () => {
describe('before image is loaded', () => {
it('renders loading spinner', () => {
- expect(wrapper.find(GlLoadingIcon)).toExist();
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
});
diff --git a/spec/frontend/design_management/pages/index_spec.js b/spec/frontend/design_management/pages/index_spec.js
index ce79feae2e7..427161a391b 100644
--- a/spec/frontend/design_management/pages/index_spec.js
+++ b/spec/frontend/design_management/pages/index_spec.js
@@ -669,6 +669,20 @@ describe('Design management index page', () => {
expect(variables.files).toEqual(event.clipboardData.files.map((f) => new File([f], '')));
});
+ it('display original file name', () => {
+ event.clipboardData.files = [new File([new Blob()], 'test.png', { type: 'image/png' })];
+ document.dispatchEvent(event);
+
+ const [{ mutation, variables }] = mockMutate.mock.calls[0];
+ expect(mutation).toBe(uploadDesignMutation);
+ expect(variables).toStrictEqual({
+ files: expect.any(Array),
+ iid: '1',
+ projectPath: 'project-path',
+ });
+ expect(variables.files[0].name).toEqual('test.png');
+ });
+
it('renames a design if it has an image.png filename', () => {
event.clipboardData.getData = () => 'image.png';
document.dispatchEvent(event);
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index 0527c2153f4..d50ac0529d6 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -388,15 +388,24 @@ describe('diffs/components/app', () => {
wrapper.vm.jumpToFile(+1);
- expect(spy.mock.calls[spy.mock.calls.length - 1]).toEqual(['diffs/scrollToFile', '222.js']);
+ expect(spy.mock.calls[spy.mock.calls.length - 1]).toEqual([
+ 'diffs/scrollToFile',
+ { path: '222.js' },
+ ]);
store.state.diffs.currentDiffFileId = '222';
wrapper.vm.jumpToFile(+1);
- expect(spy.mock.calls[spy.mock.calls.length - 1]).toEqual(['diffs/scrollToFile', '333.js']);
+ expect(spy.mock.calls[spy.mock.calls.length - 1]).toEqual([
+ 'diffs/scrollToFile',
+ { path: '333.js' },
+ ]);
store.state.diffs.currentDiffFileId = '333';
wrapper.vm.jumpToFile(-1);
- expect(spy.mock.calls[spy.mock.calls.length - 1]).toEqual(['diffs/scrollToFile', '222.js']);
+ expect(spy.mock.calls[spy.mock.calls.length - 1]).toEqual([
+ 'diffs/scrollToFile',
+ { path: '222.js' },
+ ]);
});
it('does not jump to previous file from the first one', async () => {
@@ -702,23 +711,4 @@ describe('diffs/components/app', () => {
);
});
});
-
- describe('fluid layout', () => {
- beforeEach(() => {
- setFixtures(
- '<div><div class="merge-request-container limit-container-width container-limited"></div></div>',
- );
- });
-
- it('removes limited container classes when on diffs tab', () => {
- createComponent({ isFluidLayout: false, shouldShow: true }, () => {}, {
- glFeatures: { mrChangesFluidLayout: true },
- });
-
- const containerClassList = document.querySelector('.merge-request-container').classList;
-
- expect(containerClassList).not.toContain('container-limited');
- expect(containerClassList).not.toContain('limit-container-width');
- });
- });
});
diff --git a/spec/frontend/diffs/components/diff_discussions_spec.js b/spec/frontend/diffs/components/diff_discussions_spec.js
index bd6f4cd2545..c847a79435a 100644
--- a/spec/frontend/diffs/components/diff_discussions_spec.js
+++ b/spec/frontend/diffs/components/diff_discussions_spec.js
@@ -1,6 +1,7 @@
import { GlIcon } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
+import { discussionIntersectionObserverHandlerFactory } from '~/diffs/utils/discussions';
import { createStore } from '~/mr_notes/stores';
import DiscussionNotes from '~/notes/components/discussion_notes.vue';
import NoteableDiscussion from '~/notes/components/noteable_discussion.vue';
@@ -19,6 +20,9 @@ describe('DiffDiscussions', () => {
store = createStore();
wrapper = mount(localVue.extend(DiffDiscussions), {
store,
+ provide: {
+ discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
+ },
propsData: {
discussions: getDiscussionsMockData(),
...props,
diff --git a/spec/frontend/diffs/components/diff_file_header_spec.js b/spec/frontend/diffs/components/diff_file_header_spec.js
index b16ef8fe6b0..342b4bfcc50 100644
--- a/spec/frontend/diffs/components/diff_file_header_spec.js
+++ b/spec/frontend/diffs/components/diff_file_header_spec.js
@@ -7,7 +7,7 @@ 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 { SET_DIFF_FILE_VIEWED, 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';
@@ -23,6 +23,7 @@ jest.mock('~/lib/utils/common_utils');
const diffFile = Object.freeze(
Object.assign(diffDiscussionsMockData.diff_file, {
id: '123',
+ file_hash: 'xyz',
file_identifier_hash: 'abc',
edit_path: 'link:/to/edit/path',
blob: {
@@ -58,7 +59,7 @@ describe('DiffFileHeader component', () => {
toggleFileDiscussions: jest.fn(),
toggleFileDiscussionWrappers: jest.fn(),
toggleFullDiff: jest.fn(),
- toggleActiveFileByHash: jest.fn(),
+ setCurrentFileHash: jest.fn(),
setFileCollapsedByUser: jest.fn(),
reviewFile: jest.fn(),
},
@@ -240,18 +241,19 @@ describe('DiffFileHeader component', () => {
});
describe('for any file', () => {
- const otherModes = Object.keys(diffViewerModes).filter((m) => m !== 'mode_changed');
+ const allModes = Object.keys(diffViewerModes).map((m) => [m]);
- it('for mode_changed file mode displays mode changes', () => {
+ it.each(allModes)('for %s file mode displays mode changes', (mode) => {
createComponent({
props: {
diffFile: {
...diffFile,
+ mode_changed: true,
a_mode: 'old-mode',
b_mode: 'new-mode',
viewer: {
...diffFile.viewer,
- name: diffViewerModes.mode_changed,
+ name: diffViewerModes[mode],
},
},
},
@@ -259,13 +261,14 @@ describe('DiffFileHeader component', () => {
expect(findModeChangedLine().text()).toMatch(/old-mode.+new-mode/);
});
- it.each(otherModes.map((m) => [m]))(
+ it.each(allModes.filter((m) => m[0] !== 'mode_changed'))(
'for %s file mode does not display mode changes',
(mode) => {
createComponent({
props: {
diffFile: {
...diffFile,
+ mode_changed: false,
a_mode: 'old-mode',
b_mode: 'new-mode',
viewer: {
@@ -553,7 +556,13 @@ describe('DiffFileHeader component', () => {
reviewFile,
{ file, reviewed: true },
{},
- [{ type: SET_MR_FILE_REVIEWS, payload: { [file.file_identifier_hash]: [file.id] } }],
+ [
+ { type: SET_DIFF_FILE_VIEWED, payload: { id: file.file_hash, seen: true } },
+ {
+ type: SET_MR_FILE_REVIEWS,
+ payload: { [file.file_identifier_hash]: [file.id, `hash:${file.file_hash}`] },
+ },
+ ],
[],
);
});
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 a192f7e2e9a..0ccf996e220 100644
--- a/spec/frontend/diffs/components/diff_line_note_form_spec.js
+++ b/spec/frontend/diffs/components/diff_line_note_form_spec.js
@@ -1,10 +1,18 @@
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import DiffLineNoteForm from '~/diffs/components/diff_line_note_form.vue';
import { createStore } from '~/mr_notes/stores';
import NoteForm from '~/notes/components/note_form.vue';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { noteableDataMock } from '../../notes/mock_data';
import diffFileMockData from '../mock_data/diff_file';
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => {
+ return {
+ confirmAction: jest.fn(),
+ };
+});
+
describe('DiffLineNoteForm', () => {
let wrapper;
let diffFile;
@@ -24,57 +32,68 @@ describe('DiffLineNoteForm', () => {
return shallowMount(DiffLineNoteForm, {
store,
propsData: {
- diffFileHash: diffFile.file_hash,
- diffLines,
- line: diffLines[0],
- noteTargetLine: diffLines[0],
+ ...{
+ diffFileHash: diffFile.file_hash,
+ diffLines,
+ line: diffLines[1],
+ range: { start: diffLines[0], end: diffLines[1] },
+ noteTargetLine: diffLines[1],
+ },
+ ...(args.props || {}),
},
});
};
+ const findNoteForm = () => wrapper.findComponent(NoteForm);
+
describe('methods', () => {
beforeEach(() => {
wrapper = createComponent();
});
describe('handleCancelCommentForm', () => {
+ afterEach(() => {
+ confirmAction.mockReset();
+ });
+
it('should ask for confirmation when shouldConfirm and isDirty passed as truthy', () => {
- jest.spyOn(window, 'confirm').mockReturnValue(false);
+ confirmAction.mockResolvedValueOnce(false);
- wrapper.vm.handleCancelCommentForm(true, true);
+ findNoteForm().vm.$emit('cancelForm', true, true);
- expect(window.confirm).toHaveBeenCalled();
+ expect(confirmAction).toHaveBeenCalled();
});
- it('should ask for confirmation when one of the params false', () => {
- jest.spyOn(window, 'confirm').mockReturnValue(false);
+ it('should not ask for confirmation when one of the params false', () => {
+ confirmAction.mockResolvedValueOnce(false);
- wrapper.vm.handleCancelCommentForm(true, false);
+ findNoteForm().vm.$emit('cancelForm', true, false);
- expect(window.confirm).not.toHaveBeenCalled();
+ expect(confirmAction).not.toHaveBeenCalled();
- wrapper.vm.handleCancelCommentForm(false, true);
+ findNoteForm().vm.$emit('cancelForm', false, true);
- expect(window.confirm).not.toHaveBeenCalled();
+ expect(confirmAction).not.toHaveBeenCalled();
});
- it('should call cancelCommentForm with lineCode', (done) => {
- jest.spyOn(window, 'confirm').mockImplementation(() => {});
+ it('should call cancelCommentForm with lineCode', async () => {
+ confirmAction.mockResolvedValueOnce(true);
jest.spyOn(wrapper.vm, 'cancelCommentForm').mockImplementation(() => {});
jest.spyOn(wrapper.vm, 'resetAutoSave').mockImplementation(() => {});
- wrapper.vm.handleCancelCommentForm();
- expect(window.confirm).not.toHaveBeenCalled();
- wrapper.vm.$nextTick(() => {
- expect(wrapper.vm.cancelCommentForm).toHaveBeenCalledWith({
- lineCode: diffLines[0].line_code,
- fileHash: wrapper.vm.diffFileHash,
- });
+ findNoteForm().vm.$emit('cancelForm', true, true);
+
+ await nextTick();
+
+ expect(confirmAction).toHaveBeenCalled();
- expect(wrapper.vm.resetAutoSave).toHaveBeenCalled();
+ await nextTick();
- done();
+ expect(wrapper.vm.cancelCommentForm).toHaveBeenCalledWith({
+ lineCode: diffLines[1].line_code,
+ fileHash: wrapper.vm.diffFileHash,
});
+ expect(wrapper.vm.resetAutoSave).toHaveBeenCalled();
});
});
@@ -88,13 +107,13 @@ describe('DiffLineNoteForm', () => {
start: {
line_code: wrapper.vm.commentLineStart.line_code,
type: wrapper.vm.commentLineStart.type,
- new_line: 1,
+ new_line: 2,
old_line: null,
},
end: {
line_code: wrapper.vm.line.line_code,
type: wrapper.vm.line.type,
- new_line: 1,
+ new_line: 2,
old_line: null,
},
};
@@ -118,9 +137,25 @@ describe('DiffLineNoteForm', () => {
});
});
+ describe('created', () => {
+ it('should use the provided `range` of lines', () => {
+ wrapper = createComponent();
+
+ expect(wrapper.vm.lines.start).toBe(diffLines[0]);
+ expect(wrapper.vm.lines.end).toBe(diffLines[1]);
+ });
+
+ it("should fill the internal `lines` data with the provided `line` if there's no provided `range", () => {
+ wrapper = createComponent({ props: { range: null } });
+
+ expect(wrapper.vm.lines.start).toBe(diffLines[1]);
+ expect(wrapper.vm.lines.end).toBe(diffLines[1]);
+ });
+ });
+
describe('mounted', () => {
it('should init autosave', () => {
- const key = 'autosave/Note/Issue/98//DiffNote//1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1';
+ const key = 'autosave/Note/Issue/98//DiffNote//1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2';
wrapper = createComponent();
expect(wrapper.vm.autosave).toBeDefined();
diff --git a/spec/frontend/diffs/components/tree_list_spec.js b/spec/frontend/diffs/components/tree_list_spec.js
index f316a9fdf01..31044b0818c 100644
--- a/spec/frontend/diffs/components/tree_list_spec.js
+++ b/spec/frontend/diffs/components/tree_list_spec.js
@@ -113,7 +113,9 @@ describe('Diffs tree list component', () => {
wrapper.find('.file-row').trigger('click');
- expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('diffs/scrollToFile', 'app/index.js');
+ expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('diffs/scrollToFile', {
+ path: 'app/index.js',
+ });
});
it('renders as file list when renderTreeList is false', () => {
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index 85734e05aeb..b5003a54917 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -99,6 +99,10 @@ describe('DiffsStoreActions', () => {
const projectPath = '/root/project';
const dismissEndpoint = '/-/user_callouts';
const showSuggestPopover = false;
+ const mrReviews = {
+ a: ['z', 'hash:a'],
+ b: ['y', 'hash:a'],
+ };
testAction(
setBaseConfig,
@@ -110,6 +114,7 @@ describe('DiffsStoreActions', () => {
projectPath,
dismissEndpoint,
showSuggestPopover,
+ mrReviews,
},
{
endpoint: '',
@@ -131,8 +136,21 @@ describe('DiffsStoreActions', () => {
projectPath,
dismissEndpoint,
showSuggestPopover,
+ mrReviews,
},
},
+ {
+ type: types.SET_DIFF_FILE_VIEWED,
+ payload: { id: 'z', seen: true },
+ },
+ {
+ type: types.SET_DIFF_FILE_VIEWED,
+ payload: { id: 'a', seen: true },
+ },
+ {
+ type: types.SET_DIFF_FILE_VIEWED,
+ payload: { id: 'y', seen: true },
+ },
],
[],
done,
@@ -190,10 +208,10 @@ describe('DiffsStoreActions', () => {
{ type: types.SET_RETRIEVING_BATCHES, payload: true },
{ type: types.SET_DIFF_DATA_BATCH, payload: { diff_files: res1.diff_files } },
{ type: types.SET_BATCH_LOADING_STATE, payload: 'loaded' },
- { type: types.VIEW_DIFF_FILE, payload: 'test' },
+ { type: types.SET_CURRENT_DIFF_FILE, payload: 'test' },
{ type: types.SET_DIFF_DATA_BATCH, payload: { diff_files: res2.diff_files } },
{ type: types.SET_BATCH_LOADING_STATE, payload: 'loaded' },
- { type: types.VIEW_DIFF_FILE, payload: 'test2' },
+ { type: types.SET_CURRENT_DIFF_FILE, payload: 'test2' },
{ type: types.SET_RETRIEVING_BATCHES, payload: false },
{ type: types.SET_BATCH_LOADING_STATE, payload: 'error' },
],
@@ -307,7 +325,7 @@ describe('DiffsStoreActions', () => {
it('should mark currently selected diff and set lineHash and fileHash of highlightedRow', () => {
testAction(setHighlightedRow, 'ABC_123', {}, [
{ type: types.SET_HIGHLIGHTED_ROW, payload: 'ABC_123' },
- { type: types.VIEW_DIFF_FILE, payload: 'ABC' },
+ { type: types.SET_CURRENT_DIFF_FILE, payload: 'ABC' },
]);
});
});
@@ -890,12 +908,12 @@ describe('DiffsStoreActions', () => {
},
};
- scrollToFile({ state, commit, getters }, 'path');
+ scrollToFile({ state, commit, getters }, { path: 'path' });
expect(document.location.hash).toBe('#test');
});
- it('commits VIEW_DIFF_FILE', () => {
+ it('commits SET_CURRENT_DIFF_FILE', () => {
const state = {
treeEntries: {
path: {
@@ -904,9 +922,9 @@ describe('DiffsStoreActions', () => {
},
};
- scrollToFile({ state, commit, getters }, 'path');
+ scrollToFile({ state, commit, getters }, { path: 'path' });
- expect(commit).toHaveBeenCalledWith(types.VIEW_DIFF_FILE, 'test');
+ expect(commit).toHaveBeenCalledWith(types.SET_CURRENT_DIFF_FILE, 'test');
});
});
@@ -1428,7 +1446,7 @@ describe('DiffsStoreActions', () => {
});
describe('setCurrentDiffFileIdFromNote', () => {
- it('commits VIEW_DIFF_FILE', () => {
+ it('commits SET_CURRENT_DIFF_FILE', () => {
const commit = jest.fn();
const state = { diffFiles: [{ file_hash: '123' }] };
const rootGetters = {
@@ -1438,10 +1456,10 @@ describe('DiffsStoreActions', () => {
setCurrentDiffFileIdFromNote({ commit, state, rootGetters }, '1');
- expect(commit).toHaveBeenCalledWith(types.VIEW_DIFF_FILE, '123');
+ expect(commit).toHaveBeenCalledWith(types.SET_CURRENT_DIFF_FILE, '123');
});
- it('does not commit VIEW_DIFF_FILE when discussion has no diff_file', () => {
+ it('does not commit SET_CURRENT_DIFF_FILE when discussion has no diff_file', () => {
const commit = jest.fn();
const state = { diffFiles: [{ file_hash: '123' }] };
const rootGetters = {
@@ -1454,7 +1472,7 @@ describe('DiffsStoreActions', () => {
expect(commit).not.toHaveBeenCalled();
});
- it('does not commit VIEW_DIFF_FILE when diff file does not exist', () => {
+ it('does not commit SET_CURRENT_DIFF_FILE when diff file does not exist', () => {
const commit = jest.fn();
const state = { diffFiles: [{ file_hash: '123' }] };
const rootGetters = {
@@ -1469,12 +1487,12 @@ describe('DiffsStoreActions', () => {
});
describe('navigateToDiffFileIndex', () => {
- it('commits VIEW_DIFF_FILE', (done) => {
+ it('commits SET_CURRENT_DIFF_FILE', (done) => {
testAction(
navigateToDiffFileIndex,
0,
{ diffFiles: [{ file_hash: '123' }] },
- [{ type: types.VIEW_DIFF_FILE, payload: '123' }],
+ [{ type: types.SET_CURRENT_DIFF_FILE, payload: '123' }],
[],
done,
);
@@ -1523,13 +1541,14 @@ describe('DiffsStoreActions', () => {
describe('reviewFile', () => {
const file = {
id: '123',
+ file_hash: 'xyz',
file_identifier_hash: 'abc',
load_collapsed_diff_url: 'gitlab-org/gitlab-test/-/merge_requests/1/diffs',
};
it.each`
- reviews | diffFile | reviewed
- ${{ abc: ['123'] }} | ${file} | ${true}
- ${{}} | ${file} | ${false}
+ reviews | diffFile | reviewed
+ ${{ abc: ['123', 'hash:xyz'] }} | ${file} | ${true}
+ ${{}} | ${file} | ${false}
`(
'sets reviews ($reviews) to localStorage and state for file $file if it is marked reviewed=$reviewed',
({ reviews, diffFile, reviewed }) => {
diff --git a/spec/frontend/diffs/store/mutations_spec.js b/spec/frontend/diffs/store/mutations_spec.js
index fc9ba223d5a..c104fcd5fb9 100644
--- a/spec/frontend/diffs/store/mutations_spec.js
+++ b/spec/frontend/diffs/store/mutations_spec.js
@@ -633,16 +633,36 @@ describe('DiffsStoreMutations', () => {
});
});
- describe('VIEW_DIFF_FILE', () => {
+ describe('SET_CURRENT_DIFF_FILE', () => {
it('updates currentDiffFileId', () => {
const state = createState();
- mutations[types.VIEW_DIFF_FILE](state, 'somefileid');
+ mutations[types.SET_CURRENT_DIFF_FILE](state, 'somefileid');
expect(state.currentDiffFileId).toBe('somefileid');
});
});
+ describe('SET_DIFF_FILE_VIEWED', () => {
+ let state;
+
+ beforeEach(() => {
+ state = {
+ viewedDiffFileIds: { 123: true },
+ };
+ });
+
+ it.each`
+ id | bool | outcome
+ ${'abc'} | ${true} | ${{ 123: true, abc: true }}
+ ${'123'} | ${false} | ${{ 123: false }}
+ `('sets the viewed files list to $bool for the id $id', ({ id, bool, outcome }) => {
+ mutations[types.SET_DIFF_FILE_VIEWED](state, { id, seen: bool });
+
+ expect(state.viewedDiffFileIds).toEqual(outcome);
+ });
+ });
+
describe('Set highlighted row', () => {
it('sets highlighted row', () => {
const state = createState();
diff --git a/spec/frontend/diffs/utils/diff_line_spec.js b/spec/frontend/diffs/utils/diff_line_spec.js
new file mode 100644
index 00000000000..adcb4a4433c
--- /dev/null
+++ b/spec/frontend/diffs/utils/diff_line_spec.js
@@ -0,0 +1,30 @@
+import { pickDirection } from '~/diffs/utils/diff_line';
+
+describe('diff_line utilities', () => {
+ describe('pickDirection', () => {
+ const left = {
+ line_code: 'left',
+ };
+ const right = {
+ line_code: 'right',
+ };
+ const defaultLine = {
+ left,
+ right,
+ };
+
+ it.each`
+ code | pick | line | pickDescription
+ ${'left'} | ${left} | ${defaultLine} | ${'the left line'}
+ ${'right'} | ${right} | ${defaultLine} | ${'the right line'}
+ ${'junk'} | ${left} | ${defaultLine} | ${'the default: the left line'}
+ ${'junk'} | ${right} | ${{ right }} | ${"the right line if there's no left line to default to"}
+ ${'right'} | ${left} | ${{ left }} | ${"the left line when there isn't a right line to match"}
+ `(
+ 'when provided a line and a line code `$code`, picks $pickDescription',
+ ({ code, line, pick }) => {
+ expect(pickDirection({ line, code })).toBe(pick);
+ },
+ );
+ });
+});
diff --git a/spec/frontend/diffs/utils/discussions_spec.js b/spec/frontend/diffs/utils/discussions_spec.js
new file mode 100644
index 00000000000..9a3d442d943
--- /dev/null
+++ b/spec/frontend/diffs/utils/discussions_spec.js
@@ -0,0 +1,133 @@
+import { discussionIntersectionObserverHandlerFactory } from '~/diffs/utils/discussions';
+
+describe('Diff Discussions Utils', () => {
+ describe('discussionIntersectionObserverHandlerFactory', () => {
+ it('creates a handler function', () => {
+ expect(discussionIntersectionObserverHandlerFactory()).toBeInstanceOf(Function);
+ });
+
+ describe('intersection observer handler', () => {
+ const functions = {
+ setCurrentDiscussionId: jest.fn(),
+ getPreviousUnresolvedDiscussionId: jest.fn().mockImplementation((id) => {
+ return Number(id) - 1;
+ }),
+ };
+ const defaultProcessableWrapper = {
+ entry: {
+ time: 0,
+ isIntersecting: true,
+ rootBounds: {
+ bottom: 0,
+ },
+ boundingClientRect: {
+ top: 0,
+ },
+ },
+ currentDiscussion: {
+ id: 1,
+ },
+ isFirstUnresolved: false,
+ isDiffsPage: true,
+ };
+ let handler;
+ let getMock;
+ let setMock;
+
+ beforeEach(() => {
+ functions.setCurrentDiscussionId.mockClear();
+ functions.getPreviousUnresolvedDiscussionId.mockClear();
+
+ defaultProcessableWrapper.functions = functions;
+
+ setMock = functions.setCurrentDiscussionId.mock;
+ getMock = functions.getPreviousUnresolvedDiscussionId.mock;
+ handler = discussionIntersectionObserverHandlerFactory();
+ });
+
+ it('debounces multiple simultaneous requests into one queue', () => {
+ handler(defaultProcessableWrapper);
+ handler(defaultProcessableWrapper);
+ handler(defaultProcessableWrapper);
+ handler(defaultProcessableWrapper);
+
+ expect(setTimeout).toHaveBeenCalledTimes(4);
+ expect(clearTimeout).toHaveBeenCalledTimes(3);
+
+ // By only advancing to one timer, we ensure it's all being batched into one queue
+ jest.advanceTimersToNextTimer();
+
+ expect(functions.setCurrentDiscussionId).toHaveBeenCalledTimes(4);
+ });
+
+ it('properly processes, sorts and executes the correct actions for a set of observed intersections', () => {
+ handler(defaultProcessableWrapper);
+ handler({
+ // This observation is here to be filtered out because it's a scrollDown
+ ...defaultProcessableWrapper,
+ entry: {
+ ...defaultProcessableWrapper.entry,
+ isIntersecting: false,
+ boundingClientRect: { top: 10 },
+ rootBounds: { bottom: 100 },
+ },
+ });
+ handler({
+ ...defaultProcessableWrapper,
+ entry: {
+ ...defaultProcessableWrapper.entry,
+ time: 101,
+ isIntersecting: false,
+ rootBounds: { bottom: -100 },
+ },
+ currentDiscussion: { id: 20 },
+ });
+ handler({
+ ...defaultProcessableWrapper,
+ entry: {
+ ...defaultProcessableWrapper.entry,
+ time: 100,
+ isIntersecting: false,
+ boundingClientRect: { top: 100 },
+ },
+ currentDiscussion: { id: 30 },
+ isDiffsPage: false,
+ });
+ handler({
+ ...defaultProcessableWrapper,
+ isFirstUnresolved: true,
+ entry: {
+ ...defaultProcessableWrapper.entry,
+ time: 100,
+ isIntersecting: false,
+ boundingClientRect: { top: 200 },
+ },
+ });
+
+ jest.advanceTimersToNextTimer();
+
+ expect(setMock.calls.length).toBe(4);
+ expect(setMock.calls[0]).toEqual([1]);
+ expect(setMock.calls[1]).toEqual([29]);
+ expect(setMock.calls[2]).toEqual([null]);
+ expect(setMock.calls[3]).toEqual([19]);
+
+ expect(getMock.calls.length).toBe(2);
+ expect(getMock.calls[0]).toEqual([30, false]);
+ expect(getMock.calls[1]).toEqual([20, true]);
+
+ [
+ setMock.invocationCallOrder[0],
+ getMock.invocationCallOrder[0],
+ setMock.invocationCallOrder[1],
+ setMock.invocationCallOrder[2],
+ getMock.invocationCallOrder[1],
+ setMock.invocationCallOrder[3],
+ ].forEach((order, idx, list) => {
+ // Compare each invocation sequence to the one before it (except the first one)
+ expect(list[idx - 1] || -1).toBeLessThan(order);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/diffs/utils/file_reviews_spec.js b/spec/frontend/diffs/utils/file_reviews_spec.js
index 230ec12409c..ccd27a5ae3e 100644
--- a/spec/frontend/diffs/utils/file_reviews_spec.js
+++ b/spec/frontend/diffs/utils/file_reviews_spec.js
@@ -11,14 +11,14 @@ import {
function getDefaultReviews() {
return {
- abc: ['123', '098'],
+ abc: ['123', 'hash:xyz', '098', 'hash:uvw'],
};
}
describe('File Review(s) utilities', () => {
const mrPath = 'my/fake/mr/42';
const storageKey = `${mrPath}-file-reviews`;
- const file = { id: '123', file_identifier_hash: 'abc' };
+ const file = { id: '123', file_hash: 'xyz', file_identifier_hash: 'abc' };
const storedValue = JSON.stringify(getDefaultReviews());
let reviews;
@@ -44,14 +44,14 @@ describe('File Review(s) utilities', () => {
});
describe('reviewStatuses', () => {
- const file1 = { id: '123', file_identifier_hash: 'abc' };
- const file2 = { id: '098', file_identifier_hash: 'abc' };
+ const file1 = { id: '123', hash: 'xyz', file_identifier_hash: 'abc' };
+ const file2 = { id: '098', hash: 'uvw', file_identifier_hash: 'abc' };
it.each`
mrReviews | files | fileReviews
${{}} | ${[file1, file2]} | ${{ 123: false, '098': false }}
- ${{ abc: ['123'] }} | ${[file1, file2]} | ${{ 123: true, '098': false }}
- ${{ abc: ['098'] }} | ${[file1, file2]} | ${{ 123: false, '098': true }}
+ ${{ abc: ['123', 'hash:xyz'] }} | ${[file1, file2]} | ${{ 123: true, '098': false }}
+ ${{ abc: ['098', 'hash:uvw'] }} | ${[file1, file2]} | ${{ 123: false, '098': true }}
${{ def: ['123'] }} | ${[file1, file2]} | ${{ 123: false, '098': false }}
${{ abc: ['123'], def: ['098'] }} | ${[]} | ${{}}
`(
@@ -128,7 +128,7 @@ describe('File Review(s) utilities', () => {
describe('markFileReview', () => {
it("adds a review when there's nothing that already exists", () => {
- expect(markFileReview(null, file)).toStrictEqual({ abc: ['123'] });
+ expect(markFileReview(null, file)).toStrictEqual({ abc: ['123', 'hash:xyz'] });
});
it("overwrites an existing review if it's for the same file (identifier hash)", () => {
@@ -136,15 +136,15 @@ describe('File Review(s) utilities', () => {
});
it('removes a review from the list when `reviewed` is `false`', () => {
- expect(markFileReview(reviews, file, false)).toStrictEqual({ abc: ['098'] });
+ expect(markFileReview(reviews, file, false)).toStrictEqual({ abc: ['098', 'hash:uvw'] });
});
it('adds a new review if the file ID is new', () => {
- const updatedFile = { ...file, id: '098' };
- const allReviews = markFileReview({ abc: ['123'] }, updatedFile);
+ const updatedFile = { ...file, id: '098', file_hash: 'uvw' };
+ const allReviews = markFileReview({ abc: ['123', 'hash:xyz'] }, updatedFile);
expect(allReviews).toStrictEqual(getDefaultReviews());
- expect(allReviews.abc).toStrictEqual(['123', '098']);
+ expect(allReviews.abc).toStrictEqual(['123', 'hash:xyz', '098', 'hash:uvw']);
});
it.each`
@@ -158,7 +158,7 @@ describe('File Review(s) utilities', () => {
it('removes the file key if there are no more reviews for it', () => {
let updated = markFileReview(reviews, file, false);
- updated = markFileReview(updated, { ...file, id: '098' }, false);
+ updated = markFileReview(updated, { ...file, id: '098', file_hash: 'uvw' }, false);
expect(updated).toStrictEqual({});
});
diff --git a/spec/frontend/dropzone_input_spec.js b/spec/frontend/dropzone_input_spec.js
index acf7d0780cd..12e10f7c5f4 100644
--- a/spec/frontend/dropzone_input_spec.js
+++ b/spec/frontend/dropzone_input_spec.js
@@ -71,6 +71,7 @@ describe('dropzone_input', () => {
triggerPasteEvent({
types: ['text/plain', 'text/html', 'text/rtf', 'Files'],
getData: () => longFileName,
+ files: [new File([new Blob()], longFileName, { type: 'image/png' })],
items: [
{
kind: 'file',
@@ -84,6 +85,24 @@ describe('dropzone_input', () => {
await waitForPromises();
expect(axiosMock.history.post[0].data.get('file').name).toHaveLength(246);
});
+
+ it('display original file name in comment box', async () => {
+ const axiosMock = new MockAdapter(axios);
+ triggerPasteEvent({
+ types: ['Files'],
+ files: [new File([new Blob()], 'test.png', { type: 'image/png' })],
+ items: [
+ {
+ kind: 'file',
+ type: 'image/png',
+ getAsFile: () => new Blob(),
+ },
+ ],
+ });
+ axiosMock.onPost().reply(httpStatusCodes.OK, { link: { markdown: 'foo' } });
+ await waitForPromises();
+ expect(axiosMock.history.post[0].data.get('file').name).toEqual('test.png');
+ });
});
describe('shows error message', () => {
diff --git a/spec/frontend/editor/helpers.js b/spec/frontend/editor/helpers.js
new file mode 100644
index 00000000000..6f7cdf6efb3
--- /dev/null
+++ b/spec/frontend/editor/helpers.js
@@ -0,0 +1,53 @@
+export class MyClassExtension {
+ // eslint-disable-next-line class-methods-use-this
+ provides() {
+ return {
+ shared: () => 'extension',
+ classExtMethod: () => 'class own method',
+ };
+ }
+}
+
+export function MyFnExtension() {
+ return {
+ fnExtMethod: () => 'fn own method',
+ provides: () => {
+ return {
+ fnExtMethod: () => 'class own method',
+ };
+ },
+ };
+}
+
+export const MyConstExt = () => {
+ return {
+ provides: () => {
+ return {
+ constExtMethod: () => 'const own method',
+ };
+ },
+ };
+};
+
+export const conflictingExtensions = {
+ WithInstanceExt: () => {
+ return {
+ provides: () => {
+ return {
+ use: () => 'A conflict with instance',
+ ownMethod: () => 'Non-conflicting method',
+ };
+ },
+ };
+ },
+ WithAnotherExt: () => {
+ return {
+ provides: () => {
+ return {
+ shared: () => 'A conflict with extension',
+ ownMethod: () => 'Non-conflicting method',
+ };
+ },
+ };
+ },
+};
diff --git a/spec/frontend/editor/source_editor_extension_base_spec.js b/spec/frontend/editor/source_editor_extension_base_spec.js
index 2c06ae03892..a0fb1178b3b 100644
--- a/spec/frontend/editor/source_editor_extension_base_spec.js
+++ b/spec/frontend/editor/source_editor_extension_base_spec.js
@@ -148,7 +148,10 @@ describe('The basis for an Source Editor extension', () => {
revealLineInCenter: revealSpy,
deltaDecorations: decorationsSpy,
};
- const defaultDecorationOptions = { isWholeLine: true, className: 'active-line-text' };
+ const defaultDecorationOptions = {
+ isWholeLine: true,
+ className: 'active-line-text',
+ };
useFakeRequestAnimationFrame();
@@ -157,18 +160,22 @@ describe('The basis for an Source Editor extension', () => {
});
it.each`
- desc | hash | shouldReveal | expectedRange
- ${'properly decorates a single line'} | ${'#L10'} | ${true} | ${[10, 1, 10, 1]}
- ${'properly decorates multiple lines'} | ${'#L7-42'} | ${true} | ${[7, 1, 42, 1]}
- ${'correctly highlights if lines are reversed'} | ${'#L42-7'} | ${true} | ${[7, 1, 42, 1]}
- ${'highlights one line if start/end are the same'} | ${'#L7-7'} | ${true} | ${[7, 1, 7, 1]}
- ${'does not highlight if there is no hash'} | ${''} | ${false} | ${null}
- ${'does not highlight if the hash is undefined'} | ${undefined} | ${false} | ${null}
- ${'does not highlight if hash is incomplete 1'} | ${'#L'} | ${false} | ${null}
- ${'does not highlight if hash is incomplete 2'} | ${'#L-'} | ${false} | ${null}
- `('$desc', ({ hash, shouldReveal, expectedRange } = {}) => {
+ desc | hash | bounds | shouldReveal | expectedRange
+ ${'properly decorates a single line'} | ${'#L10'} | ${undefined} | ${true} | ${[10, 1, 10, 1]}
+ ${'properly decorates multiple lines'} | ${'#L7-42'} | ${undefined} | ${true} | ${[7, 1, 42, 1]}
+ ${'correctly highlights if lines are reversed'} | ${'#L42-7'} | ${undefined} | ${true} | ${[7, 1, 42, 1]}
+ ${'highlights one line if start/end are the same'} | ${'#L7-7'} | ${undefined} | ${true} | ${[7, 1, 7, 1]}
+ ${'does not highlight if there is no hash'} | ${''} | ${undefined} | ${false} | ${null}
+ ${'does not highlight if the hash is undefined'} | ${undefined} | ${undefined} | ${false} | ${null}
+ ${'does not highlight if hash is incomplete 1'} | ${'#L'} | ${undefined} | ${false} | ${null}
+ ${'does not highlight if hash is incomplete 2'} | ${'#L-'} | ${undefined} | ${false} | ${null}
+ ${'highlights lines if bounds are passed'} | ${undefined} | ${[17, 42]} | ${true} | ${[17, 1, 42, 1]}
+ ${'highlights one line if bounds has a single value'} | ${undefined} | ${[17]} | ${true} | ${[17, 1, 17, 1]}
+ ${'does not highlight if bounds is invalid'} | ${undefined} | ${[Number.NaN]} | ${false} | ${null}
+ ${'uses bounds if both hash and bounds exist'} | ${'#L7-42'} | ${[3, 5]} | ${true} | ${[3, 1, 5, 1]}
+ `('$desc', ({ hash, bounds, shouldReveal, expectedRange } = {}) => {
window.location.hash = hash;
- SourceEditorExtension.highlightLines(instance);
+ SourceEditorExtension.highlightLines(instance, bounds);
if (!shouldReveal) {
expect(revealSpy).not.toHaveBeenCalled();
expect(decorationsSpy).not.toHaveBeenCalled();
@@ -193,6 +200,43 @@ describe('The basis for an Source Editor extension', () => {
SourceEditorExtension.highlightLines(instance);
expect(instance.lineDecorations).toBe('foo');
});
+
+ it('replaces existing line highlights', () => {
+ const oldLineDecorations = [
+ {
+ range: new Range(1, 1, 20, 1),
+ options: { isWholeLine: true, className: 'active-line-text' },
+ },
+ ];
+ const newLineDecorations = [
+ {
+ range: new Range(7, 1, 10, 1),
+ options: { isWholeLine: true, className: 'active-line-text' },
+ },
+ ];
+ instance.lineDecorations = oldLineDecorations;
+ SourceEditorExtension.highlightLines(instance, [7, 10]);
+ expect(decorationsSpy).toHaveBeenCalledWith(oldLineDecorations, newLineDecorations);
+ });
+ });
+
+ describe('removeHighlights', () => {
+ const decorationsSpy = jest.fn();
+ const lineDecorations = [
+ {
+ range: new Range(1, 1, 20, 1),
+ options: { isWholeLine: true, className: 'active-line-text' },
+ },
+ ];
+ const instance = {
+ deltaDecorations: decorationsSpy,
+ lineDecorations,
+ };
+
+ it('removes all existing decorations', () => {
+ SourceEditorExtension.removeHighlights(instance);
+ expect(decorationsSpy).toHaveBeenCalledWith(lineDecorations, []);
+ });
});
describe('setupLineLinking', () => {
diff --git a/spec/frontend/editor/source_editor_extension_spec.js b/spec/frontend/editor/source_editor_extension_spec.js
new file mode 100644
index 00000000000..6f2eb07a043
--- /dev/null
+++ b/spec/frontend/editor/source_editor_extension_spec.js
@@ -0,0 +1,65 @@
+import EditorExtension from '~/editor/source_editor_extension';
+import { EDITOR_EXTENSION_DEFINITION_ERROR } from '~/editor/constants';
+import * as helpers from './helpers';
+
+describe('Editor Extension', () => {
+ const dummyObj = { foo: 'bar' };
+
+ it.each`
+ definition | setupOptions
+ ${undefined} | ${undefined}
+ ${undefined} | ${{}}
+ ${undefined} | ${dummyObj}
+ ${{}} | ${dummyObj}
+ ${dummyObj} | ${dummyObj}
+ `(
+ 'throws when definition = $definition and setupOptions = $setupOptions',
+ ({ definition, setupOptions }) => {
+ const constructExtension = () => new EditorExtension({ definition, setupOptions });
+ expect(constructExtension).toThrowError(EDITOR_EXTENSION_DEFINITION_ERROR);
+ },
+ );
+
+ it.each`
+ definition | setupOptions | expectedName
+ ${helpers.MyClassExtension} | ${undefined} | ${'MyClassExtension'}
+ ${helpers.MyClassExtension} | ${{}} | ${'MyClassExtension'}
+ ${helpers.MyClassExtension} | ${dummyObj} | ${'MyClassExtension'}
+ ${helpers.MyFnExtension} | ${undefined} | ${'MyFnExtension'}
+ ${helpers.MyFnExtension} | ${{}} | ${'MyFnExtension'}
+ ${helpers.MyFnExtension} | ${dummyObj} | ${'MyFnExtension'}
+ ${helpers.MyConstExt} | ${undefined} | ${'MyConstExt'}
+ ${helpers.MyConstExt} | ${{}} | ${'MyConstExt'}
+ ${helpers.MyConstExt} | ${dummyObj} | ${'MyConstExt'}
+ `(
+ 'correctly creates extension for definition = $definition and setupOptions = $setupOptions',
+ ({ definition, setupOptions, expectedName }) => {
+ const extension = new EditorExtension({ definition, setupOptions });
+ // eslint-disable-next-line new-cap
+ const constructedDefinition = new definition();
+
+ expect(extension).toEqual(
+ expect.objectContaining({
+ name: expectedName,
+ setupOptions,
+ }),
+ );
+ expect(extension.obj.constructor.prototype).toBe(constructedDefinition.constructor.prototype);
+ },
+ );
+
+ describe('api', () => {
+ it.each`
+ definition | expectedKeys
+ ${helpers.MyClassExtension} | ${['shared', 'classExtMethod']}
+ ${helpers.MyFnExtension} | ${['fnExtMethod']}
+ ${helpers.MyConstExt} | ${['constExtMethod']}
+ `('correctly returns API for $definition', ({ definition, expectedKeys }) => {
+ const extension = new EditorExtension({ definition });
+ const expectedApi = Object.fromEntries(
+ expectedKeys.map((key) => [key, expect.any(Function)]),
+ );
+ expect(extension.api).toEqual(expect.objectContaining(expectedApi));
+ });
+ });
+});
diff --git a/spec/frontend/editor/source_editor_instance_spec.js b/spec/frontend/editor/source_editor_instance_spec.js
new file mode 100644
index 00000000000..87b20a4ba73
--- /dev/null
+++ b/spec/frontend/editor/source_editor_instance_spec.js
@@ -0,0 +1,387 @@
+import { editor as monacoEditor } from 'monaco-editor';
+import {
+ EDITOR_EXTENSION_NAMING_CONFLICT_ERROR,
+ EDITOR_EXTENSION_NO_DEFINITION_ERROR,
+ EDITOR_EXTENSION_DEFINITION_TYPE_ERROR,
+ EDITOR_EXTENSION_NOT_REGISTERED_ERROR,
+ EDITOR_EXTENSION_NOT_SPECIFIED_FOR_UNUSE_ERROR,
+} from '~/editor/constants';
+import Instance from '~/editor/source_editor_instance';
+import { sprintf } from '~/locale';
+import { MyClassExtension, conflictingExtensions, MyFnExtension, MyConstExt } from './helpers';
+
+describe('Source Editor Instance', () => {
+ let seInstance;
+
+ const defSetupOptions = { foo: 'bar' };
+ const fullExtensionsArray = [
+ { definition: MyClassExtension },
+ { definition: MyFnExtension },
+ { definition: MyConstExt },
+ ];
+ const fullExtensionsArrayWithOptions = [
+ { definition: MyClassExtension, setupOptions: defSetupOptions },
+ { definition: MyFnExtension, setupOptions: defSetupOptions },
+ { definition: MyConstExt, setupOptions: defSetupOptions },
+ ];
+
+ const fooFn = jest.fn();
+ class DummyExt {
+ // eslint-disable-next-line class-methods-use-this
+ provides() {
+ return {
+ fooFn,
+ };
+ }
+ }
+
+ afterEach(() => {
+ seInstance = undefined;
+ });
+
+ it('sets up the registry for the methods coming from extensions', () => {
+ seInstance = new Instance();
+ expect(seInstance.methods).toBeDefined();
+
+ seInstance.use({ definition: MyClassExtension });
+ expect(seInstance.methods).toEqual({
+ shared: 'MyClassExtension',
+ classExtMethod: 'MyClassExtension',
+ });
+
+ seInstance.use({ definition: MyFnExtension });
+ expect(seInstance.methods).toEqual({
+ shared: 'MyClassExtension',
+ classExtMethod: 'MyClassExtension',
+ fnExtMethod: 'MyFnExtension',
+ });
+ });
+
+ describe('proxy', () => {
+ it('returns prop from an extension if extension provides it', () => {
+ seInstance = new Instance();
+ seInstance.use({ definition: DummyExt });
+
+ expect(fooFn).not.toHaveBeenCalled();
+ seInstance.fooFn();
+ expect(fooFn).toHaveBeenCalled();
+ });
+
+ it('returns props from SE instance itself if no extension provides the prop', () => {
+ seInstance = new Instance({
+ use: fooFn,
+ });
+ jest.spyOn(seInstance, 'use').mockImplementation(() => {});
+ expect(seInstance.use).not.toHaveBeenCalled();
+ expect(fooFn).not.toHaveBeenCalled();
+ seInstance.use();
+ expect(seInstance.use).toHaveBeenCalled();
+ expect(fooFn).not.toHaveBeenCalled();
+ });
+
+ it('returns props from Monaco instance when the prop does not exist on the SE instance', () => {
+ seInstance = new Instance({
+ fooFn,
+ });
+
+ expect(fooFn).not.toHaveBeenCalled();
+ seInstance.fooFn();
+ expect(fooFn).toHaveBeenCalled();
+ });
+ });
+
+ describe('public API', () => {
+ it.each(['use', 'unuse'], 'provides "%s" as public method by default', (method) => {
+ seInstance = new Instance();
+ expect(seInstance[method]).toBeDefined();
+ });
+
+ describe('use', () => {
+ it('extends the SE instance with methods provided by an extension', () => {
+ seInstance = new Instance();
+ seInstance.use({ definition: DummyExt });
+
+ expect(fooFn).not.toHaveBeenCalled();
+ seInstance.fooFn();
+ expect(fooFn).toHaveBeenCalled();
+ });
+
+ it.each`
+ extensions | expectedProps
+ ${{ definition: MyClassExtension }} | ${['shared', 'classExtMethod']}
+ ${{ definition: MyFnExtension }} | ${['fnExtMethod']}
+ ${{ definition: MyConstExt }} | ${['constExtMethod']}
+ ${fullExtensionsArray} | ${['shared', 'classExtMethod', 'fnExtMethod', 'constExtMethod']}
+ ${fullExtensionsArrayWithOptions} | ${['shared', 'classExtMethod', 'fnExtMethod', 'constExtMethod']}
+ `(
+ 'Should register $expectedProps when extension is "$extensions"',
+ ({ extensions, expectedProps }) => {
+ seInstance = new Instance();
+ expect(seInstance.extensionsAPI).toHaveLength(0);
+
+ seInstance.use(extensions);
+
+ expect(seInstance.extensionsAPI).toEqual(expectedProps);
+ },
+ );
+
+ it.each`
+ definition | preInstalledExtDefinition | expectedErrorProp
+ ${conflictingExtensions.WithInstanceExt} | ${MyClassExtension} | ${'use'}
+ ${conflictingExtensions.WithInstanceExt} | ${null} | ${'use'}
+ ${conflictingExtensions.WithAnotherExt} | ${null} | ${undefined}
+ ${conflictingExtensions.WithAnotherExt} | ${MyClassExtension} | ${'shared'}
+ ${MyClassExtension} | ${conflictingExtensions.WithAnotherExt} | ${'shared'}
+ `(
+ 'logs the naming conflict error when registering $definition',
+ ({ definition, preInstalledExtDefinition, expectedErrorProp }) => {
+ seInstance = new Instance();
+ jest.spyOn(console, 'error').mockImplementation(() => {});
+
+ if (preInstalledExtDefinition) {
+ seInstance.use({ definition: preInstalledExtDefinition });
+ // eslint-disable-next-line no-console
+ expect(console.error).not.toHaveBeenCalled();
+ }
+
+ seInstance.use({ definition });
+
+ if (expectedErrorProp) {
+ // eslint-disable-next-line no-console
+ expect(console.error).toHaveBeenCalledWith(
+ expect.any(String),
+ expect.stringContaining(
+ sprintf(EDITOR_EXTENSION_NAMING_CONFLICT_ERROR, { prop: expectedErrorProp }),
+ ),
+ );
+ } else {
+ // eslint-disable-next-line no-console
+ expect(console.error).not.toHaveBeenCalled();
+ }
+ },
+ );
+
+ it.each`
+ extensions | thrownError
+ ${''} | ${EDITOR_EXTENSION_NO_DEFINITION_ERROR}
+ ${undefined} | ${EDITOR_EXTENSION_NO_DEFINITION_ERROR}
+ ${{}} | ${EDITOR_EXTENSION_NO_DEFINITION_ERROR}
+ ${{ foo: 'bar' }} | ${EDITOR_EXTENSION_NO_DEFINITION_ERROR}
+ ${{ definition: '' }} | ${EDITOR_EXTENSION_NO_DEFINITION_ERROR}
+ ${{ definition: undefined }} | ${EDITOR_EXTENSION_NO_DEFINITION_ERROR}
+ ${{ definition: [] }} | ${EDITOR_EXTENSION_DEFINITION_TYPE_ERROR}
+ ${{ definition: {} }} | ${EDITOR_EXTENSION_DEFINITION_TYPE_ERROR}
+ ${{ definition: { foo: 'bar' } }} | ${EDITOR_EXTENSION_DEFINITION_TYPE_ERROR}
+ `(
+ 'Should throw $thrownError when extension is "$extensions"',
+ ({ extensions, thrownError }) => {
+ seInstance = new Instance();
+ const useExtension = () => {
+ seInstance.use(extensions);
+ };
+ expect(useExtension).toThrowError(thrownError);
+ },
+ );
+
+ describe('global extensions registry', () => {
+ let extensionStore;
+
+ beforeEach(() => {
+ extensionStore = new Map();
+ seInstance = new Instance({}, extensionStore);
+ });
+
+ it('stores _instances_ of the used extensions in a global registry', () => {
+ const extension = seInstance.use({ definition: MyClassExtension });
+
+ expect(extensionStore.size).toBe(1);
+ expect(extensionStore.entries().next().value).toEqual(['MyClassExtension', extension]);
+ });
+
+ it('does not duplicate entries in the registry', () => {
+ jest.spyOn(extensionStore, 'set');
+
+ const extension1 = seInstance.use({ definition: MyClassExtension });
+ seInstance.use({ definition: MyClassExtension });
+
+ expect(extensionStore.set).toHaveBeenCalledTimes(1);
+ expect(extensionStore.set).toHaveBeenCalledWith('MyClassExtension', extension1);
+ });
+
+ it.each`
+ desc | currentSetupOptions | newSetupOptions | expectedCallTimes
+ ${'updates'} | ${undefined} | ${defSetupOptions} | ${2}
+ ${'updates'} | ${defSetupOptions} | ${undefined} | ${2}
+ ${'updates'} | ${{ foo: 'bar' }} | ${{ foo: 'new' }} | ${2}
+ ${'does not update'} | ${undefined} | ${undefined} | ${1}
+ ${'does not update'} | ${{}} | ${{}} | ${1}
+ ${'does not update'} | ${defSetupOptions} | ${defSetupOptions} | ${1}
+ `(
+ '$desc the extensions entry when setupOptions "$currentSetupOptions" get changed to "$newSetupOptions"',
+ ({ currentSetupOptions, newSetupOptions, expectedCallTimes }) => {
+ jest.spyOn(extensionStore, 'set');
+
+ const extension1 = seInstance.use({
+ definition: MyClassExtension,
+ setupOptions: currentSetupOptions,
+ });
+ const extension2 = seInstance.use({
+ definition: MyClassExtension,
+ setupOptions: newSetupOptions,
+ });
+
+ expect(extensionStore.size).toBe(1);
+ expect(extensionStore.set).toHaveBeenCalledTimes(expectedCallTimes);
+ if (expectedCallTimes > 1) {
+ expect(extensionStore.set).toHaveBeenCalledWith('MyClassExtension', extension2);
+ } else {
+ expect(extensionStore.set).toHaveBeenCalledWith('MyClassExtension', extension1);
+ }
+ },
+ );
+ });
+ });
+
+ describe('unuse', () => {
+ it.each`
+ unuseExtension | thrownError
+ ${undefined} | ${EDITOR_EXTENSION_NOT_SPECIFIED_FOR_UNUSE_ERROR}
+ ${''} | ${EDITOR_EXTENSION_NOT_SPECIFIED_FOR_UNUSE_ERROR}
+ ${{}} | ${sprintf(EDITOR_EXTENSION_NOT_REGISTERED_ERROR, { name: '' })}
+ ${[]} | ${EDITOR_EXTENSION_NOT_SPECIFIED_FOR_UNUSE_ERROR}
+ `(
+ `Should throw "${EDITOR_EXTENSION_NOT_SPECIFIED_FOR_UNUSE_ERROR}" when extension is "$unuseExtension"`,
+ ({ unuseExtension, thrownError }) => {
+ seInstance = new Instance();
+ const unuse = () => {
+ seInstance.unuse(unuseExtension);
+ };
+ expect(unuse).toThrowError(thrownError);
+ },
+ );
+
+ it.each`
+ initExtensions | unuseExtensionIndex | remainingAPI
+ ${{ definition: MyClassExtension }} | ${0} | ${[]}
+ ${{ definition: MyFnExtension }} | ${0} | ${[]}
+ ${{ definition: MyConstExt }} | ${0} | ${[]}
+ ${fullExtensionsArray} | ${0} | ${['fnExtMethod', 'constExtMethod']}
+ ${fullExtensionsArray} | ${1} | ${['shared', 'classExtMethod', 'constExtMethod']}
+ ${fullExtensionsArray} | ${2} | ${['shared', 'classExtMethod', 'fnExtMethod']}
+ `(
+ 'un-registers properties introduced by single extension $unuseExtension',
+ ({ initExtensions, unuseExtensionIndex, remainingAPI }) => {
+ seInstance = new Instance();
+ const extensions = seInstance.use(initExtensions);
+
+ if (Array.isArray(initExtensions)) {
+ seInstance.unuse(extensions[unuseExtensionIndex]);
+ } else {
+ seInstance.unuse(extensions);
+ }
+ expect(seInstance.extensionsAPI).toEqual(remainingAPI);
+ },
+ );
+
+ it.each`
+ unuseExtensionIndex | remainingAPI
+ ${[0, 1]} | ${['constExtMethod']}
+ ${[0, 2]} | ${['fnExtMethod']}
+ ${[1, 2]} | ${['shared', 'classExtMethod']}
+ `(
+ 'un-registers properties introduced by multiple extensions $unuseExtension',
+ ({ unuseExtensionIndex, remainingAPI }) => {
+ seInstance = new Instance();
+ const extensions = seInstance.use(fullExtensionsArray);
+ const extensionsToUnuse = extensions.filter((ext, index) =>
+ unuseExtensionIndex.includes(index),
+ );
+
+ seInstance.unuse(extensionsToUnuse);
+ expect(seInstance.extensionsAPI).toEqual(remainingAPI);
+ },
+ );
+
+ it('it does not remove entry from the global registry to keep for potential future re-use', () => {
+ const extensionStore = new Map();
+ seInstance = new Instance({}, extensionStore);
+ const extensions = seInstance.use(fullExtensionsArray);
+ const verifyExpectations = () => {
+ const entries = extensionStore.entries();
+ const mockExtensions = ['MyClassExtension', 'MyFnExtension', 'MyConstExt'];
+ expect(extensionStore.size).toBe(mockExtensions.length);
+ mockExtensions.forEach((ext, index) => {
+ expect(entries.next().value).toEqual([ext, extensions[index]]);
+ });
+ };
+
+ verifyExpectations();
+ seInstance.unuse(extensions);
+ verifyExpectations();
+ });
+ });
+
+ describe('updateModelLanguage', () => {
+ let instanceModel;
+
+ beforeEach(() => {
+ instanceModel = monacoEditor.createModel('');
+ seInstance = new Instance({
+ getModel: () => instanceModel,
+ });
+ });
+
+ it.each`
+ path | expectedLanguage
+ ${'foo.js'} | ${'javascript'}
+ ${'foo.md'} | ${'markdown'}
+ ${'foo.rb'} | ${'ruby'}
+ ${''} | ${'plaintext'}
+ ${undefined} | ${'plaintext'}
+ ${'test.nonexistingext'} | ${'plaintext'}
+ `(
+ 'changes language of an attached model to "$expectedLanguage" when filepath is "$path"',
+ ({ path, expectedLanguage }) => {
+ seInstance.updateModelLanguage(path);
+ expect(instanceModel.getLanguageIdentifier().language).toBe(expectedLanguage);
+ },
+ );
+ });
+
+ describe('extensions life-cycle callbacks', () => {
+ const onSetup = jest.fn().mockImplementation(() => {});
+ const onUse = jest.fn().mockImplementation(() => {});
+ const onBeforeUnuse = jest.fn().mockImplementation(() => {});
+ const onUnuse = jest.fn().mockImplementation(() => {});
+ const MyFullExtWithCallbacks = () => {
+ return {
+ onSetup,
+ onUse,
+ onBeforeUnuse,
+ onUnuse,
+ };
+ };
+
+ it('passes correct arguments to callback fns when using an extension', () => {
+ seInstance = new Instance();
+ seInstance.use({
+ definition: MyFullExtWithCallbacks,
+ setupOptions: defSetupOptions,
+ });
+ expect(onSetup).toHaveBeenCalledWith(defSetupOptions, seInstance);
+ expect(onUse).toHaveBeenCalledWith(seInstance);
+ });
+
+ it('passes correct arguments to callback fns when un-using an extension', () => {
+ seInstance = new Instance();
+ const extension = seInstance.use({
+ definition: MyFullExtWithCallbacks,
+ setupOptions: defSetupOptions,
+ });
+ seInstance.unuse(extension);
+ expect(onBeforeUnuse).toHaveBeenCalledWith(seInstance);
+ expect(onUnuse).toHaveBeenCalledWith(seInstance);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/editor/source_editor_yaml_ext_spec.js b/spec/frontend/editor/source_editor_yaml_ext_spec.js
new file mode 100644
index 00000000000..97d2b0b21d0
--- /dev/null
+++ b/spec/frontend/editor/source_editor_yaml_ext_spec.js
@@ -0,0 +1,449 @@
+import { Document } from 'yaml';
+import SourceEditor from '~/editor/source_editor';
+import { YamlEditorExtension } from '~/editor/extensions/source_editor_yaml_ext';
+import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base';
+
+const getEditorInstance = (editorInstanceOptions = {}) => {
+ setFixtures('<div id="editor"></div>');
+ return new SourceEditor().createInstance({
+ el: document.getElementById('editor'),
+ blobPath: '.gitlab-ci.yml',
+ language: 'yaml',
+ ...editorInstanceOptions,
+ });
+};
+
+const getEditorInstanceWithExtension = (extensionOptions = {}, editorInstanceOptions = {}) => {
+ setFixtures('<div id="editor"></div>');
+ const instance = getEditorInstance(editorInstanceOptions);
+ instance.use(new YamlEditorExtension({ instance, ...extensionOptions }));
+
+ // Remove the below once
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/325992 is resolved
+ if (editorInstanceOptions.value && !extensionOptions.model) {
+ instance.setValue(editorInstanceOptions.value);
+ }
+
+ return instance;
+};
+
+describe('YamlCreatorExtension', () => {
+ describe('constructor', () => {
+ it('saves constructor options', () => {
+ const instance = getEditorInstanceWithExtension({
+ highlightPath: 'foo',
+ enableComments: true,
+ });
+ expect(instance).toEqual(
+ expect.objectContaining({
+ options: expect.objectContaining({
+ highlightPath: 'foo',
+ enableComments: true,
+ }),
+ }),
+ );
+ });
+
+ it('dumps values loaded with the model constructor options', () => {
+ const model = { foo: 'bar' };
+ const expected = 'foo: bar\n';
+ const instance = getEditorInstanceWithExtension({ model });
+ expect(instance.getDoc().get('foo')).toBeDefined();
+ expect(instance.getValue()).toEqual(expected);
+ });
+
+ it('registers the onUpdate() function', () => {
+ const instance = getEditorInstance();
+ const onDidChangeModelContent = jest.spyOn(instance, 'onDidChangeModelContent');
+ instance.use(new YamlEditorExtension({ instance }));
+ expect(onDidChangeModelContent).toHaveBeenCalledWith(expect.any(Function));
+ });
+
+ it("If not provided with a load constructor option, it will parse the editor's value", () => {
+ const editorValue = 'foo: bar';
+ const instance = getEditorInstanceWithExtension({}, { value: editorValue });
+ expect(instance.getDoc().get('foo')).toBeDefined();
+ });
+
+ it("Prefers values loaded with the load constructor option over the editor's existing value", () => {
+ const editorValue = 'oldValue: this should be overriden';
+ const model = { thisShould: 'be the actual value' };
+ const expected = 'thisShould: be the actual value\n';
+ const instance = getEditorInstanceWithExtension({ model }, { value: editorValue });
+ expect(instance.getDoc().get('oldValue')).toBeUndefined();
+ expect(instance.getValue()).toEqual(expected);
+ });
+ });
+
+ describe('initFromModel', () => {
+ const model = { foo: 'bar', 1: 2, abc: ['def'] };
+ const doc = new Document(model);
+
+ it('should call transformComments if enableComments is true', () => {
+ const instance = getEditorInstanceWithExtension({ enableComments: true });
+ const transformComments = jest.spyOn(YamlEditorExtension, 'transformComments');
+ YamlEditorExtension.initFromModel(instance, model);
+ expect(transformComments).toHaveBeenCalled();
+ });
+
+ it('should not call transformComments if enableComments is false', () => {
+ const instance = getEditorInstanceWithExtension({ enableComments: false });
+ const transformComments = jest.spyOn(YamlEditorExtension, 'transformComments');
+ YamlEditorExtension.initFromModel(instance, model);
+ expect(transformComments).not.toHaveBeenCalled();
+ });
+
+ it('should call setValue with the stringified model', () => {
+ const instance = getEditorInstanceWithExtension();
+ const setValue = jest.spyOn(instance, 'setValue');
+ YamlEditorExtension.initFromModel(instance, model);
+ expect(setValue).toHaveBeenCalledWith(doc.toString());
+ });
+ });
+
+ describe('wrapCommentString', () => {
+ const longString =
+ 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.';
+
+ it('should add spaces before each line', () => {
+ const result = YamlEditorExtension.wrapCommentString(longString);
+ const lines = result.split('\n');
+ expect(lines.every((ln) => ln.startsWith(' '))).toBe(true);
+ });
+
+ it('should break long comments into lines of max. 79 chars', () => {
+ // 79 = 80 char width minus 1 char for the '#' at the start of each line
+ const result = YamlEditorExtension.wrapCommentString(longString);
+ const lines = result.split('\n');
+ expect(lines.every((ln) => ln.length <= 79)).toBe(true);
+ });
+
+ it('should decrease the line width if passed a level by 2 chars per level', () => {
+ for (let i = 0; i <= 5; i += 1) {
+ const result = YamlEditorExtension.wrapCommentString(longString, i);
+ const lines = result.split('\n');
+ const decreaseLineWidthBy = i * 2;
+ const maxLineWith = 79 - decreaseLineWidthBy;
+ const isValidLine = (ln) => {
+ if (ln.length <= maxLineWith) return true;
+ // The line may exceed the max line width in case the word is the
+ // only one in the line and thus cannot be broken further
+ return ln.split(' ').length <= 1;
+ };
+ expect(lines.every(isValidLine)).toBe(true);
+ }
+ });
+
+ it('return null if passed an invalid string value', () => {
+ expect(YamlEditorExtension.wrapCommentString(null)).toBe(null);
+ expect(YamlEditorExtension.wrapCommentString()).toBe(null);
+ });
+
+ it('throw an error if passed an invalid level value', () => {
+ expect(() => YamlEditorExtension.wrapCommentString('abc', -5)).toThrow(
+ 'Invalid value "-5" for variable `level`',
+ );
+ expect(() => YamlEditorExtension.wrapCommentString('abc', 'invalid')).toThrow(
+ 'Invalid value "invalid" for variable `level`',
+ );
+ });
+ });
+
+ describe('transformComments', () => {
+ const getInstanceWithModel = (model) => {
+ return getEditorInstanceWithExtension({
+ model,
+ enableComments: true,
+ });
+ };
+
+ it('converts comments inside an array', () => {
+ const model = ['# test comment', 'def', '# foo', 999];
+ const expected = `# test comment\n- def\n# foo\n- 999\n`;
+ const instance = getInstanceWithModel(model);
+ expect(instance.getValue()).toEqual(expected);
+ });
+
+ it('converts generic comments inside an object and places them at the top', () => {
+ const model = { foo: 'bar', 1: 2, '#': 'test comment' };
+ const expected = `# test comment\n"1": 2\nfoo: bar\n`;
+ const instance = getInstanceWithModel(model);
+ expect(instance.getValue()).toEqual(expected);
+ });
+
+ it('adds specific comments before the mentioned entry of an object', () => {
+ const model = { foo: 'bar', 1: 2, '#|foo': 'foo comment' };
+ const expected = `"1": 2\n# foo comment\nfoo: bar\n`;
+ const instance = getInstanceWithModel(model);
+ expect(instance.getValue()).toEqual(expected);
+ });
+
+ it('limits long comments to 80 char width, including indentation', () => {
+ const model = {
+ '#|foo':
+ 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.',
+ foo: {
+ nested1: {
+ nested2: {
+ nested3: {
+ '#|bar':
+ 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.',
+ bar: 'baz',
+ },
+ },
+ },
+ },
+ };
+ const expected = `# Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy
+# eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam
+# voluptua. At vero eos et accusam et justo duo dolores et ea rebum.
+foo:
+ nested1:
+ nested2:
+ nested3:
+ # Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam
+ # nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat,
+ # sed diam voluptua. At vero eos et accusam et justo duo dolores et ea
+ # rebum.
+ bar: baz
+`;
+ const instance = getInstanceWithModel(model);
+ expect(instance.getValue()).toEqual(expected);
+ });
+ });
+
+ describe('getDoc', () => {
+ it('returns a yaml `Document` Type', () => {
+ const instance = getEditorInstanceWithExtension();
+ expect(instance.getDoc()).toBeInstanceOf(Document);
+ });
+ });
+
+ describe('setDoc', () => {
+ const model = { foo: 'bar', 1: 2, abc: ['def'] };
+ const doc = new Document(model);
+
+ it('should call transformComments if enableComments is true', () => {
+ const spy = jest.spyOn(YamlEditorExtension, 'transformComments');
+ const instance = getEditorInstanceWithExtension({ enableComments: true });
+ instance.setDoc(doc);
+ expect(spy).toHaveBeenCalledWith(doc);
+ });
+
+ it('should not call transformComments if enableComments is false', () => {
+ const spy = jest.spyOn(YamlEditorExtension, 'transformComments');
+ const instance = getEditorInstanceWithExtension({ enableComments: false });
+ instance.setDoc(doc);
+ expect(spy).not.toHaveBeenCalled();
+ });
+
+ it("should call setValue with the stringified doc if the editor's value is empty", () => {
+ const instance = getEditorInstanceWithExtension();
+ const setValue = jest.spyOn(instance, 'setValue');
+ const updateValue = jest.spyOn(instance, 'updateValue');
+ instance.setDoc(doc);
+ expect(setValue).toHaveBeenCalledWith(doc.toString());
+ expect(updateValue).not.toHaveBeenCalled();
+ });
+
+ it("should call updateValue with the stringified doc if the editor's value is not empty", () => {
+ const instance = getEditorInstanceWithExtension({}, { value: 'asjkdhkasjdh' });
+ const setValue = jest.spyOn(instance, 'setValue');
+ const updateValue = jest.spyOn(instance, 'updateValue');
+ instance.setDoc(doc);
+ expect(setValue).not.toHaveBeenCalled();
+ expect(updateValue).toHaveBeenCalledWith(doc.toString());
+ });
+
+ it('should trigger the onUpdate method', () => {
+ const instance = getEditorInstanceWithExtension();
+ const onUpdate = jest.spyOn(instance, 'onUpdate');
+ instance.setDoc(doc);
+ expect(onUpdate).toHaveBeenCalled();
+ });
+ });
+
+ describe('getDataModel', () => {
+ it('returns the model as JS', () => {
+ const value = 'abc: def\nfoo:\n - bar\n - baz\n';
+ const expected = { abc: 'def', foo: ['bar', 'baz'] };
+ const instance = getEditorInstanceWithExtension({}, { value });
+ expect(instance.getDataModel()).toEqual(expected);
+ });
+ });
+
+ describe('setDataModel', () => {
+ it('sets the value to a YAML-representation of the Doc', () => {
+ const model = {
+ abc: ['def'],
+ '#|foo': 'foo comment',
+ foo: {
+ '#|abc': 'abc comment',
+ abc: [{ def: 'ghl', lorem: 'ipsum' }, '# array comment', null],
+ bar: 'baz',
+ },
+ };
+ const expected =
+ 'abc:\n' +
+ ' - def\n' +
+ '# foo comment\n' +
+ 'foo:\n' +
+ ' # abc comment\n' +
+ ' abc:\n' +
+ ' - def: ghl\n' +
+ ' lorem: ipsum\n' +
+ ' # array comment\n' +
+ ' - null\n' +
+ ' bar: baz\n';
+
+ const instance = getEditorInstanceWithExtension({ enableComments: true });
+ const setValue = jest.spyOn(instance, 'setValue');
+
+ instance.setDataModel(model);
+
+ expect(setValue).toHaveBeenCalledWith(expected);
+ });
+
+ it('causes the editor value to be updated', () => {
+ const initialModel = { foo: 'this should be overriden' };
+ const initialValue = 'foo: this should be overriden\n';
+ const newValue = { thisShould: 'be the actual value' };
+ const expected = 'thisShould: be the actual value\n';
+ const instance = getEditorInstanceWithExtension({ model: initialModel });
+ expect(instance.getValue()).toEqual(initialValue);
+ instance.setDataModel(newValue);
+ expect(instance.getValue()).toEqual(expected);
+ });
+ });
+
+ describe('onUpdate', () => {
+ it('calls highlight', () => {
+ const highlightPath = 'foo';
+ const instance = getEditorInstanceWithExtension({ highlightPath });
+ instance.highlight = jest.fn();
+ instance.onUpdate();
+ expect(instance.highlight).toHaveBeenCalledWith(highlightPath);
+ });
+ });
+
+ describe('updateValue', () => {
+ it("causes the editor's value to be updated", () => {
+ const oldValue = 'foobar';
+ const newValue = 'bazboo';
+ const instance = getEditorInstanceWithExtension({}, { value: oldValue });
+ instance.updateValue(newValue);
+ expect(instance.getValue()).toEqual(newValue);
+ });
+ });
+
+ describe('highlight', () => {
+ const highlightPathOnSetup = 'abc';
+ const value = `foo:
+ bar:
+ - baz
+ - boo
+ abc: def
+`;
+ let instance;
+ let highlightLinesSpy;
+ let removeHighlightsSpy;
+
+ beforeEach(() => {
+ instance = getEditorInstanceWithExtension({ highlightPath: highlightPathOnSetup }, { value });
+ highlightLinesSpy = jest.spyOn(SourceEditorExtension, 'highlightLines');
+ removeHighlightsSpy = jest.spyOn(SourceEditorExtension, 'removeHighlights');
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('saves the highlighted path in highlightPath', () => {
+ const path = 'foo.bar';
+ instance.highlight(path);
+ expect(instance.options.highlightPath).toEqual(path);
+ });
+
+ it('calls highlightLines with a number of lines', () => {
+ const path = 'foo.bar';
+ instance.highlight(path);
+ expect(highlightLinesSpy).toHaveBeenCalledWith(instance, [2, 4]);
+ });
+
+ it('calls removeHighlights if path is null', () => {
+ instance.highlight(null);
+ expect(removeHighlightsSpy).toHaveBeenCalledWith(instance);
+ expect(highlightLinesSpy).not.toHaveBeenCalled();
+ expect(instance.options.highlightPath).toBeNull();
+ });
+
+ it('throws an error if path is invalid and does not change the highlighted path', () => {
+ expect(() => instance.highlight('invalidPath[0]')).toThrow(
+ 'The node invalidPath[0] could not be found inside the document.',
+ );
+ expect(instance.options.highlightPath).toEqual(highlightPathOnSetup);
+ expect(highlightLinesSpy).not.toHaveBeenCalled();
+ expect(removeHighlightsSpy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('locate', () => {
+ const options = {
+ enableComments: true,
+ model: {
+ abc: ['def'],
+ '#|foo': 'foo comment',
+ foo: {
+ '#|abc': 'abc comment',
+ abc: [{ def: 'ghl', lorem: 'ipsum' }, '# array comment', null],
+ bar: 'baz',
+ },
+ },
+ };
+
+ const value =
+ /* 1 */ 'abc:\n' +
+ /* 2 */ ' - def\n' +
+ /* 3 */ '# foo comment\n' +
+ /* 4 */ 'foo:\n' +
+ /* 5 */ ' # abc comment\n' +
+ /* 6 */ ' abc:\n' +
+ /* 7 */ ' - def: ghl\n' +
+ /* 8 */ ' lorem: ipsum\n' +
+ /* 9 */ ' # array comment\n' +
+ /* 10 */ ' - null\n' +
+ /* 11 */ ' bar: baz\n';
+
+ it('asserts that the test setup is correct', () => {
+ const instance = getEditorInstanceWithExtension(options);
+ expect(instance.getValue()).toEqual(value);
+ });
+
+ it('returns the expected line numbers for a path to an object inside the yaml', () => {
+ const path = 'foo.abc';
+ const expected = [6, 10];
+ const instance = getEditorInstanceWithExtension(options);
+ expect(instance.locate(path)).toEqual(expected);
+ });
+
+ it('throws an error if a path cannot be found inside the yaml', () => {
+ const path = 'baz[8]';
+ const instance = getEditorInstanceWithExtension(options);
+ expect(() => instance.locate(path)).toThrow();
+ });
+
+ it('returns the expected line numbers for a path to an array entry inside the yaml', () => {
+ const path = 'foo.abc[0]';
+ const expected = [7, 8];
+ const instance = getEditorInstanceWithExtension(options);
+ expect(instance.locate(path)).toEqual(expected);
+ });
+
+ it('returns the expected line numbers for a path that includes a comment inside the yaml', () => {
+ const path = 'foo';
+ const expected = [4, 11];
+ const instance = getEditorInstanceWithExtension(options);
+ expect(instance.locate(path)).toEqual(expected);
+ });
+ });
+});
diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js
new file mode 100644
index 00000000000..e56b6448b7d
--- /dev/null
+++ b/spec/frontend/environments/graphql/mock_data.js
@@ -0,0 +1,530 @@
+export const environmentsApp = {
+ environments: [
+ {
+ name: 'review',
+ size: 2,
+ latest: {
+ id: 42,
+ global_id: 'gid://gitlab/Environment/42',
+ name: 'review/goodbye',
+ state: 'available',
+ external_url: 'https://example.org',
+ environment_type: 'review',
+ name_without_type: 'goodbye',
+ last_deployment: null,
+ has_stop_action: false,
+ rollout_status: null,
+ environment_path: '/h5bp/html5-boilerplate/-/environments/42',
+ stop_path: '/h5bp/html5-boilerplate/-/environments/42/stop',
+ cancel_auto_stop_path: '/h5bp/html5-boilerplate/-/environments/42/cancel_auto_stop',
+ delete_path: '/api/v4/projects/8/environments/42',
+ folder_path: '/h5bp/html5-boilerplate/-/environments/folders/review',
+ created_at: '2021-10-04T19:27:20.639Z',
+ updated_at: '2021-10-04T19:27:20.639Z',
+ can_stop: true,
+ logs_path: '/h5bp/html5-boilerplate/-/logs?environment_name=review%2Fgoodbye',
+ logs_api_path: '/h5bp/html5-boilerplate/-/logs/k8s.json?environment_name=review%2Fgoodbye',
+ enable_advanced_logs_querying: false,
+ can_delete: false,
+ has_opened_alert: false,
+ },
+ },
+ {
+ name: 'production',
+ size: 1,
+ latest: {
+ id: 8,
+ global_id: 'gid://gitlab/Environment/8',
+ name: 'production',
+ state: 'available',
+ external_url: 'https://example.org',
+ environment_type: null,
+ name_without_type: 'production',
+ last_deployment: {
+ id: 80,
+ iid: 24,
+ sha: '4ca0310329e8f251b892d7be205eec8b7dd220e5',
+ ref: {
+ name: 'root-master-patch-18104',
+ ref_path: '/h5bp/html5-boilerplate/-/tree/root-master-patch-18104',
+ },
+ status: 'success',
+ created_at: '2021-10-08T19:53:54.543Z',
+ deployed_at: '2021-10-08T20:02:36.763Z',
+ tag: false,
+ 'last?': true,
+ user: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ web_url: 'http://gdk.test:3000/root',
+ show_status: false,
+ path: '/root',
+ },
+ deployable: {
+ id: 911,
+ name: 'deploy-job',
+ started: '2021-10-08T19:54:00.658Z',
+ complete: true,
+ archived: false,
+ build_path: '/h5bp/html5-boilerplate/-/jobs/911',
+ retry_path: '/h5bp/html5-boilerplate/-/jobs/911/retry',
+ play_path: '/h5bp/html5-boilerplate/-/jobs/911/play',
+ playable: true,
+ scheduled: false,
+ created_at: '2021-10-08T19:53:54.482Z',
+ updated_at: '2021-10-08T20:02:36.730Z',
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'manual play action',
+ group: 'success',
+ tooltip: 'passed',
+ has_details: true,
+ details_path: '/h5bp/html5-boilerplate/-/jobs/911',
+ illustration: {
+ image:
+ '/assets/illustrations/manual_action-c55aee2c5f9ebe9f72751480af8bb307be1a6f35552f344cc6d1bf979d3422f6.svg',
+ size: 'svg-394',
+ title: 'This job requires a manual action',
+ content:
+ 'This job requires manual intervention to start. Before starting this job, you can add variables below for last-minute configuration changes.',
+ },
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
+ action: {
+ icon: 'play',
+ title: 'Play',
+ path: '/h5bp/html5-boilerplate/-/jobs/911/play',
+ method: 'post',
+ button_title: 'Trigger this manual action',
+ },
+ },
+ },
+ commit: {
+ id: '4ca0310329e8f251b892d7be205eec8b7dd220e5',
+ short_id: '4ca03103',
+ created_at: '2021-10-08T19:27:01.000+00:00',
+ parent_ids: ['b385360b15bd61391a0efbd101788d4a80387270'],
+ title: 'Update .gitlab-ci.yml',
+ message: 'Update .gitlab-ci.yml',
+ author_name: 'Administrator',
+ author_email: 'admin@example.com',
+ authored_date: '2021-10-08T19:27:01.000+00:00',
+ committer_name: 'Administrator',
+ committer_email: 'admin@example.com',
+ committed_date: '2021-10-08T19:27:01.000+00:00',
+ trailers: {},
+ web_url:
+ 'http://gdk.test:3000/h5bp/html5-boilerplate/-/commit/4ca0310329e8f251b892d7be205eec8b7dd220e5',
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ web_url: 'http://gdk.test:3000/root',
+ show_status: false,
+ path: '/root',
+ },
+ author_gravatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ commit_url:
+ 'http://gdk.test:3000/h5bp/html5-boilerplate/-/commit/4ca0310329e8f251b892d7be205eec8b7dd220e5',
+ commit_path:
+ '/h5bp/html5-boilerplate/-/commit/4ca0310329e8f251b892d7be205eec8b7dd220e5',
+ },
+ manual_actions: [],
+ scheduled_actions: [],
+ playable_build: {
+ retry_path: '/h5bp/html5-boilerplate/-/jobs/911/retry',
+ play_path: '/h5bp/html5-boilerplate/-/jobs/911/play',
+ },
+ cluster: null,
+ },
+ has_stop_action: false,
+ rollout_status: null,
+ environment_path: '/h5bp/html5-boilerplate/-/environments/8',
+ stop_path: '/h5bp/html5-boilerplate/-/environments/8/stop',
+ cancel_auto_stop_path: '/h5bp/html5-boilerplate/-/environments/8/cancel_auto_stop',
+ delete_path: '/api/v4/projects/8/environments/8',
+ folder_path: '/h5bp/html5-boilerplate/-/environments/folders/production',
+ created_at: '2021-06-17T15:09:38.599Z',
+ updated_at: '2021-10-08T19:50:44.445Z',
+ can_stop: true,
+ logs_path: '/h5bp/html5-boilerplate/-/logs?environment_name=production',
+ logs_api_path: '/h5bp/html5-boilerplate/-/logs/k8s.json?environment_name=production',
+ enable_advanced_logs_querying: false,
+ can_delete: false,
+ has_opened_alert: false,
+ },
+ },
+ {
+ name: 'staging',
+ size: 1,
+ latest: {
+ id: 7,
+ global_id: 'gid://gitlab/Environment/7',
+ name: 'staging',
+ state: 'available',
+ external_url: null,
+ environment_type: null,
+ name_without_type: 'staging',
+ last_deployment: null,
+ has_stop_action: false,
+ rollout_status: null,
+ environment_path: '/h5bp/html5-boilerplate/-/environments/7',
+ stop_path: '/h5bp/html5-boilerplate/-/environments/7/stop',
+ cancel_auto_stop_path: '/h5bp/html5-boilerplate/-/environments/7/cancel_auto_stop',
+ delete_path: '/api/v4/projects/8/environments/7',
+ folder_path: '/h5bp/html5-boilerplate/-/environments/folders/staging',
+ created_at: '2021-06-17T15:09:38.570Z',
+ updated_at: '2021-06-17T15:09:38.570Z',
+ can_stop: true,
+ logs_path: '/h5bp/html5-boilerplate/-/logs?environment_name=staging',
+ logs_api_path: '/h5bp/html5-boilerplate/-/logs/k8s.json?environment_name=staging',
+ enable_advanced_logs_querying: false,
+ can_delete: false,
+ has_opened_alert: false,
+ },
+ },
+ ],
+ review_app: {
+ can_setup_review_app: true,
+ all_clusters_empty: true,
+ review_snippet:
+ '{"deploy_review"=>{"stage"=>"deploy", "script"=>["echo \\"Deploy a review app\\""], "environment"=>{"name"=>"review/$CI_COMMIT_REF_NAME", "url"=>"https://$CI_ENVIRONMENT_SLUG.example.com"}, "only"=>["branches"]}}',
+ },
+ available_count: 4,
+ stopped_count: 0,
+};
+
+export const resolvedEnvironmentsApp = {
+ availableCount: 4,
+ environments: [
+ {
+ name: 'review',
+ size: 2,
+ latest: {
+ id: 42,
+ globalId: 'gid://gitlab/Environment/42',
+ name: 'review/goodbye',
+ state: 'available',
+ externalUrl: 'https://example.org',
+ environmentType: 'review',
+ nameWithoutType: 'goodbye',
+ lastDeployment: null,
+ hasStopAction: false,
+ rolloutStatus: null,
+ environmentPath: '/h5bp/html5-boilerplate/-/environments/42',
+ stopPath: '/h5bp/html5-boilerplate/-/environments/42/stop',
+ cancelAutoStopPath: '/h5bp/html5-boilerplate/-/environments/42/cancel_auto_stop',
+ deletePath: '/api/v4/projects/8/environments/42',
+ folderPath: '/h5bp/html5-boilerplate/-/environments/folders/review',
+ createdAt: '2021-10-04T19:27:20.639Z',
+ updatedAt: '2021-10-04T19:27:20.639Z',
+ canStop: true,
+ logsPath: '/h5bp/html5-boilerplate/-/logs?environment_name=review%2Fgoodbye',
+ logsApiPath: '/h5bp/html5-boilerplate/-/logs/k8s.json?environment_name=review%2Fgoodbye',
+ enableAdvancedLogsQuerying: false,
+ canDelete: false,
+ hasOpenedAlert: false,
+ },
+ __typename: 'NestedLocalEnvironment',
+ },
+ {
+ name: 'production',
+ size: 1,
+ latest: {
+ id: 8,
+ globalId: 'gid://gitlab/Environment/8',
+ name: 'production',
+ state: 'available',
+ externalUrl: 'https://example.org',
+ environmentType: null,
+ nameWithoutType: 'production',
+ lastDeployment: {
+ id: 80,
+ iid: 24,
+ sha: '4ca0310329e8f251b892d7be205eec8b7dd220e5',
+ ref: {
+ name: 'root-master-patch-18104',
+ refPath: '/h5bp/html5-boilerplate/-/tree/root-master-patch-18104',
+ },
+ status: 'success',
+ createdAt: '2021-10-08T19:53:54.543Z',
+ deployedAt: '2021-10-08T20:02:36.763Z',
+ tag: false,
+ 'last?': true,
+ user: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ webUrl: 'http://gdk.test:3000/root',
+ showStatus: false,
+ path: '/root',
+ },
+ deployable: {
+ id: 911,
+ name: 'deploy-job',
+ started: '2021-10-08T19:54:00.658Z',
+ complete: true,
+ archived: false,
+ buildPath: '/h5bp/html5-boilerplate/-/jobs/911',
+ retryPath: '/h5bp/html5-boilerplate/-/jobs/911/retry',
+ playPath: '/h5bp/html5-boilerplate/-/jobs/911/play',
+ playable: true,
+ scheduled: false,
+ createdAt: '2021-10-08T19:53:54.482Z',
+ updatedAt: '2021-10-08T20:02:36.730Z',
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'manual play action',
+ group: 'success',
+ tooltip: 'passed',
+ hasDetails: true,
+ detailsPath: '/h5bp/html5-boilerplate/-/jobs/911',
+ illustration: {
+ image:
+ '/assets/illustrations/manual_action-c55aee2c5f9ebe9f72751480af8bb307be1a6f35552f344cc6d1bf979d3422f6.svg',
+ size: 'svg-394',
+ title: 'This job requires a manual action',
+ content:
+ 'This job requires manual intervention to start. Before starting this job, you can add variables below for last-minute configuration changes.',
+ },
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
+ action: {
+ icon: 'play',
+ title: 'Play',
+ path: '/h5bp/html5-boilerplate/-/jobs/911/play',
+ method: 'post',
+ buttonTitle: 'Trigger this manual action',
+ },
+ },
+ },
+ commit: {
+ id: '4ca0310329e8f251b892d7be205eec8b7dd220e5',
+ shortId: '4ca03103',
+ createdAt: '2021-10-08T19:27:01.000+00:00',
+ parentIds: ['b385360b15bd61391a0efbd101788d4a80387270'],
+ title: 'Update .gitlab-ci.yml',
+ message: 'Update .gitlab-ci.yml',
+ authorName: 'Administrator',
+ authorEmail: 'admin@example.com',
+ authoredDate: '2021-10-08T19:27:01.000+00:00',
+ committerName: 'Administrator',
+ committerEmail: 'admin@example.com',
+ committedDate: '2021-10-08T19:27:01.000+00:00',
+ trailers: {},
+ webUrl:
+ 'http://gdk.test:3000/h5bp/html5-boilerplate/-/commit/4ca0310329e8f251b892d7be205eec8b7dd220e5',
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ webUrl: 'http://gdk.test:3000/root',
+ showStatus: false,
+ path: '/root',
+ },
+ authorGravatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ commitUrl:
+ 'http://gdk.test:3000/h5bp/html5-boilerplate/-/commit/4ca0310329e8f251b892d7be205eec8b7dd220e5',
+ commitPath: '/h5bp/html5-boilerplate/-/commit/4ca0310329e8f251b892d7be205eec8b7dd220e5',
+ },
+ manualActions: [],
+ scheduledActions: [],
+ playableBuild: {
+ retryPath: '/h5bp/html5-boilerplate/-/jobs/911/retry',
+ playPath: '/h5bp/html5-boilerplate/-/jobs/911/play',
+ },
+ cluster: null,
+ },
+ hasStopAction: false,
+ rolloutStatus: null,
+ environmentPath: '/h5bp/html5-boilerplate/-/environments/8',
+ stopPath: '/h5bp/html5-boilerplate/-/environments/8/stop',
+ cancelAutoStopPath: '/h5bp/html5-boilerplate/-/environments/8/cancel_auto_stop',
+ deletePath: '/api/v4/projects/8/environments/8',
+ folderPath: '/h5bp/html5-boilerplate/-/environments/folders/production',
+ createdAt: '2021-06-17T15:09:38.599Z',
+ updatedAt: '2021-10-08T19:50:44.445Z',
+ canStop: true,
+ logsPath: '/h5bp/html5-boilerplate/-/logs?environment_name=production',
+ logsApiPath: '/h5bp/html5-boilerplate/-/logs/k8s.json?environment_name=production',
+ enableAdvancedLogsQuerying: false,
+ canDelete: false,
+ hasOpenedAlert: false,
+ },
+ __typename: 'NestedLocalEnvironment',
+ },
+ {
+ name: 'staging',
+ size: 1,
+ latest: {
+ id: 7,
+ globalId: 'gid://gitlab/Environment/7',
+ name: 'staging',
+ state: 'available',
+ externalUrl: null,
+ environmentType: null,
+ nameWithoutType: 'staging',
+ lastDeployment: null,
+ hasStopAction: false,
+ rolloutStatus: null,
+ environmentPath: '/h5bp/html5-boilerplate/-/environments/7',
+ stopPath: '/h5bp/html5-boilerplate/-/environments/7/stop',
+ cancelAutoStopPath: '/h5bp/html5-boilerplate/-/environments/7/cancel_auto_stop',
+ deletePath: '/api/v4/projects/8/environments/7',
+ folderPath: '/h5bp/html5-boilerplate/-/environments/folders/staging',
+ createdAt: '2021-06-17T15:09:38.570Z',
+ updatedAt: '2021-06-17T15:09:38.570Z',
+ canStop: true,
+ logsPath: '/h5bp/html5-boilerplate/-/logs?environment_name=staging',
+ logsApiPath: '/h5bp/html5-boilerplate/-/logs/k8s.json?environment_name=staging',
+ enableAdvancedLogsQuerying: false,
+ canDelete: false,
+ hasOpenedAlert: false,
+ },
+ __typename: 'NestedLocalEnvironment',
+ },
+ ],
+ reviewApp: {
+ canSetupReviewApp: true,
+ allClustersEmpty: true,
+ reviewSnippet:
+ '{"deploy_review"=>{"stage"=>"deploy", "script"=>["echo \\"Deploy a review app\\""], "environment"=>{"name"=>"review/$CI_COMMIT_REF_NAME", "url"=>"https://$CI_ENVIRONMENT_SLUG.example.com"}, "only"=>["branches"]}}',
+ __typename: 'ReviewApp',
+ },
+ stoppedCount: 0,
+ __typename: 'LocalEnvironmentApp',
+};
+
+export const folder = {
+ environments: [
+ {
+ id: 42,
+ global_id: 'gid://gitlab/Environment/42',
+ name: 'review/goodbye',
+ state: 'available',
+ external_url: 'https://example.org',
+ environment_type: 'review',
+ name_without_type: 'goodbye',
+ last_deployment: null,
+ has_stop_action: false,
+ rollout_status: null,
+ environment_path: '/h5bp/html5-boilerplate/-/environments/42',
+ stop_path: '/h5bp/html5-boilerplate/-/environments/42/stop',
+ cancel_auto_stop_path: '/h5bp/html5-boilerplate/-/environments/42/cancel_auto_stop',
+ delete_path: '/api/v4/projects/8/environments/42',
+ folder_path: '/h5bp/html5-boilerplate/-/environments/folders/review',
+ created_at: '2021-10-04T19:27:20.639Z',
+ updated_at: '2021-10-04T19:27:20.639Z',
+ can_stop: true,
+ logs_path: '/h5bp/html5-boilerplate/-/logs?environment_name=review%2Fgoodbye',
+ logs_api_path: '/h5bp/html5-boilerplate/-/logs/k8s.json?environment_name=review%2Fgoodbye',
+ enable_advanced_logs_querying: false,
+ can_delete: false,
+ has_opened_alert: false,
+ },
+ {
+ id: 41,
+ global_id: 'gid://gitlab/Environment/41',
+ name: 'review/hello',
+ state: 'available',
+ external_url: 'https://example.org',
+ environment_type: 'review',
+ name_without_type: 'hello',
+ last_deployment: null,
+ has_stop_action: false,
+ rollout_status: null,
+ environment_path: '/h5bp/html5-boilerplate/-/environments/41',
+ stop_path: '/h5bp/html5-boilerplate/-/environments/41/stop',
+ cancel_auto_stop_path: '/h5bp/html5-boilerplate/-/environments/41/cancel_auto_stop',
+ delete_path: '/api/v4/projects/8/environments/41',
+ folder_path: '/h5bp/html5-boilerplate/-/environments/folders/review',
+ created_at: '2021-10-04T19:27:00.527Z',
+ updated_at: '2021-10-04T19:27:00.527Z',
+ can_stop: true,
+ logs_path: '/h5bp/html5-boilerplate/-/logs?environment_name=review%2Fhello',
+ logs_api_path: '/h5bp/html5-boilerplate/-/logs/k8s.json?environment_name=review%2Fhello',
+ enable_advanced_logs_querying: false,
+ can_delete: false,
+ has_opened_alert: false,
+ },
+ ],
+ available_count: 2,
+ stopped_count: 0,
+};
+
+export const resolvedFolder = {
+ availableCount: 2,
+ environments: [
+ {
+ id: 42,
+ globalId: 'gid://gitlab/Environment/42',
+ name: 'review/goodbye',
+ state: 'available',
+ externalUrl: 'https://example.org',
+ environmentType: 'review',
+ nameWithoutType: 'goodbye',
+ lastDeployment: null,
+ hasStopAction: false,
+ rolloutStatus: null,
+ environmentPath: '/h5bp/html5-boilerplate/-/environments/42',
+ stopPath: '/h5bp/html5-boilerplate/-/environments/42/stop',
+ cancelAutoStopPath: '/h5bp/html5-boilerplate/-/environments/42/cancel_auto_stop',
+ deletePath: '/api/v4/projects/8/environments/42',
+ folderPath: '/h5bp/html5-boilerplate/-/environments/folders/review',
+ createdAt: '2021-10-04T19:27:20.639Z',
+ updatedAt: '2021-10-04T19:27:20.639Z',
+ canStop: true,
+ logsPath: '/h5bp/html5-boilerplate/-/logs?environment_name=review%2Fgoodbye',
+ logsApiPath: '/h5bp/html5-boilerplate/-/logs/k8s.json?environment_name=review%2Fgoodbye',
+ enableAdvancedLogsQuerying: false,
+ canDelete: false,
+ hasOpenedAlert: false,
+ __typename: 'LocalEnvironment',
+ },
+ {
+ id: 41,
+ globalId: 'gid://gitlab/Environment/41',
+ name: 'review/hello',
+ state: 'available',
+ externalUrl: 'https://example.org',
+ environmentType: 'review',
+ nameWithoutType: 'hello',
+ lastDeployment: null,
+ hasStopAction: false,
+ rolloutStatus: null,
+ environmentPath: '/h5bp/html5-boilerplate/-/environments/41',
+ stopPath: '/h5bp/html5-boilerplate/-/environments/41/stop',
+ cancelAutoStopPath: '/h5bp/html5-boilerplate/-/environments/41/cancel_auto_stop',
+ deletePath: '/api/v4/projects/8/environments/41',
+ folderPath: '/h5bp/html5-boilerplate/-/environments/folders/review',
+ createdAt: '2021-10-04T19:27:00.527Z',
+ updatedAt: '2021-10-04T19:27:00.527Z',
+ canStop: true,
+ logsPath: '/h5bp/html5-boilerplate/-/logs?environment_name=review%2Fhello',
+ logsApiPath: '/h5bp/html5-boilerplate/-/logs/k8s.json?environment_name=review%2Fhello',
+ enableAdvancedLogsQuerying: false,
+ canDelete: false,
+ hasOpenedAlert: false,
+ __typename: 'LocalEnvironment',
+ },
+ ],
+ stoppedCount: 0,
+ __typename: 'LocalEnvironmentFolder',
+};
diff --git a/spec/frontend/environments/graphql/resolvers_spec.js b/spec/frontend/environments/graphql/resolvers_spec.js
new file mode 100644
index 00000000000..4d2a0818996
--- /dev/null
+++ b/spec/frontend/environments/graphql/resolvers_spec.js
@@ -0,0 +1,91 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import { resolvers } from '~/environments/graphql/resolvers';
+import { TEST_HOST } from 'helpers/test_constants';
+import { environmentsApp, resolvedEnvironmentsApp, folder, resolvedFolder } from './mock_data';
+
+const ENDPOINT = `${TEST_HOST}/environments`;
+
+describe('~/frontend/environments/graphql/resolvers', () => {
+ let mockResolvers;
+ let mock;
+
+ beforeEach(() => {
+ mockResolvers = resolvers(ENDPOINT);
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.reset();
+ });
+
+ describe('environmentApp', () => {
+ it('should fetch environments and map them to frontend data', async () => {
+ mock.onGet(ENDPOINT, { params: { nested: true } }).reply(200, environmentsApp);
+
+ const app = await mockResolvers.Query.environmentApp();
+ expect(app).toEqual(resolvedEnvironmentsApp);
+ });
+ });
+ describe('folder', () => {
+ it('should fetch the folder url passed to it', async () => {
+ mock.onGet(ENDPOINT, { params: { per_page: 3 } }).reply(200, folder);
+
+ const environmentFolder = await mockResolvers.Query.folder(null, {
+ environment: { folderPath: ENDPOINT },
+ });
+
+ expect(environmentFolder).toEqual(resolvedFolder);
+ });
+ });
+ describe('stopEnvironment', () => {
+ it('should post to the stop environment path', async () => {
+ mock.onPost(ENDPOINT).reply(200);
+
+ await mockResolvers.Mutations.stopEnvironment(null, { environment: { stopPath: ENDPOINT } });
+
+ expect(mock.history.post).toContainEqual(
+ expect.objectContaining({ url: ENDPOINT, method: 'post' }),
+ );
+ });
+ });
+ describe('rollbackEnvironment', () => {
+ it('should post to the retry environment path', async () => {
+ mock.onPost(ENDPOINT).reply(200);
+
+ await mockResolvers.Mutations.rollbackEnvironment(null, {
+ environment: { retryUrl: ENDPOINT },
+ });
+
+ expect(mock.history.post).toContainEqual(
+ expect.objectContaining({ url: ENDPOINT, method: 'post' }),
+ );
+ });
+ });
+ describe('deleteEnvironment', () => {
+ it('should DELETE to the delete environment path', async () => {
+ mock.onDelete(ENDPOINT).reply(200);
+
+ await mockResolvers.Mutations.deleteEnvironment(null, {
+ environment: { deletePath: ENDPOINT },
+ });
+
+ expect(mock.history.delete).toContainEqual(
+ expect.objectContaining({ url: ENDPOINT, method: 'delete' }),
+ );
+ });
+ });
+ describe('cancelAutoStop', () => {
+ it('should post to the auto stop path', async () => {
+ mock.onPost(ENDPOINT).reply(200);
+
+ await mockResolvers.Mutations.cancelAutoStop(null, {
+ environment: { autoStopPath: ENDPOINT },
+ });
+
+ expect(mock.history.post).toContainEqual(
+ expect.objectContaining({ url: ENDPOINT, method: 'post' }),
+ );
+ });
+ });
+});
diff --git a/spec/frontend/environments/new_environment_folder_spec.js b/spec/frontend/environments/new_environment_folder_spec.js
new file mode 100644
index 00000000000..5696e187a86
--- /dev/null
+++ b/spec/frontend/environments/new_environment_folder_spec.js
@@ -0,0 +1,74 @@
+import VueApollo from 'vue-apollo';
+import Vue from 'vue';
+import { GlCollapse, GlIcon } from '@gitlab/ui';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import EnvironmentsFolder from '~/environments/components/new_environment_folder.vue';
+import { s__ } from '~/locale';
+import { resolvedEnvironmentsApp, resolvedFolder } from './graphql/mock_data';
+
+Vue.use(VueApollo);
+
+describe('~/environments/components/new_environments_folder.vue', () => {
+ let wrapper;
+ let environmentFolderMock;
+ let nestedEnvironment;
+ let folderName;
+
+ const findLink = () => wrapper.findByRole('link', { name: s__('Environments|Show all') });
+
+ const createApolloProvider = () => {
+ const mockResolvers = { Query: { folder: environmentFolderMock } };
+
+ return createMockApollo([], mockResolvers);
+ };
+
+ const createWrapper = (propsData, apolloProvider) =>
+ mountExtended(EnvironmentsFolder, { apolloProvider, propsData });
+
+ beforeEach(() => {
+ environmentFolderMock = jest.fn();
+ [nestedEnvironment] = resolvedEnvironmentsApp.environments;
+ environmentFolderMock.mockReturnValue(resolvedFolder);
+ wrapper = createWrapper({ nestedEnvironment }, createApolloProvider());
+ folderName = wrapper.findByText(nestedEnvironment.name);
+ });
+
+ afterEach(() => {
+ wrapper?.destroy();
+ });
+
+ it('displays the name of the folder', () => {
+ expect(folderName.text()).toBe(nestedEnvironment.name);
+ });
+
+ describe('collapse', () => {
+ let icons;
+ let collapse;
+
+ beforeEach(() => {
+ collapse = wrapper.findComponent(GlCollapse);
+ icons = wrapper.findAllComponents(GlIcon);
+ });
+
+ it('is collapsed by default', () => {
+ const link = findLink();
+
+ expect(collapse.attributes('visible')).toBeUndefined();
+ expect(icons.wrappers.map((i) => i.props('name'))).toEqual(['angle-right', 'folder-o']);
+ expect(folderName.classes('gl-font-weight-bold')).toBe(false);
+ expect(link.exists()).toBe(false);
+ });
+
+ it('opens on click', async () => {
+ await folderName.trigger('click');
+
+ const link = findLink();
+
+ expect(collapse.attributes('visible')).toBe('true');
+ expect(icons.wrappers.map((i) => i.props('name'))).toEqual(['angle-down', 'folder-open']);
+ expect(folderName.classes('gl-font-weight-bold')).toBe(true);
+ expect(link.attributes('href')).toBe(nestedEnvironment.latest.folderPath);
+ });
+ });
+});
diff --git a/spec/frontend/environments/new_environments_app_spec.js b/spec/frontend/environments/new_environments_app_spec.js
new file mode 100644
index 00000000000..0ad8e8f442c
--- /dev/null
+++ b/spec/frontend/environments/new_environments_app_spec.js
@@ -0,0 +1,50 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { mount } from '@vue/test-utils';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import EnvironmentsApp from '~/environments/components/new_environments_app.vue';
+import EnvironmentsFolder from '~/environments/components/new_environment_folder.vue';
+import { resolvedEnvironmentsApp, resolvedFolder } from './graphql/mock_data';
+
+Vue.use(VueApollo);
+
+describe('~/environments/components/new_environments_app.vue', () => {
+ let wrapper;
+ let environmentAppMock;
+ let environmentFolderMock;
+
+ const createApolloProvider = () => {
+ const mockResolvers = {
+ Query: { environmentApp: environmentAppMock, folder: environmentFolderMock },
+ };
+
+ return createMockApollo([], mockResolvers);
+ };
+
+ const createWrapper = (apolloProvider) => mount(EnvironmentsApp, { apolloProvider });
+
+ beforeEach(() => {
+ environmentAppMock = jest.fn();
+ environmentFolderMock = jest.fn();
+ });
+
+ afterEach(() => {
+ wrapper?.destroy();
+ });
+
+ it('should show all the folders that are fetched', async () => {
+ environmentAppMock.mockReturnValue(resolvedEnvironmentsApp);
+ environmentFolderMock.mockReturnValue(resolvedFolder);
+ const apolloProvider = createApolloProvider();
+ wrapper = createWrapper(apolloProvider);
+
+ await waitForPromises();
+ await Vue.nextTick();
+
+ const text = wrapper.findAllComponents(EnvironmentsFolder).wrappers.map((w) => w.text());
+
+ expect(text).toContainEqual(expect.stringMatching('review'));
+ expect(text).not.toContainEqual(expect.stringMatching('production'));
+ });
+});
diff --git a/spec/frontend/experimentation/utils_spec.js b/spec/frontend/experimentation/utils_spec.js
index de060f5eb8c..923795ca3f3 100644
--- a/spec/frontend/experimentation/utils_spec.js
+++ b/spec/frontend/experimentation/utils_spec.js
@@ -1,4 +1,4 @@
-import { assignGitlabExperiment } from 'helpers/experimentation_helper';
+import { stubExperiments } from 'helpers/experimentation_helper';
import {
DEFAULT_VARIANT,
CANDIDATE_VARIANT,
@@ -7,15 +7,45 @@ import {
import * as experimentUtils from '~/experimentation/utils';
describe('experiment Utilities', () => {
- const TEST_KEY = 'abc';
+ const ABC_KEY = 'abc';
+ const DEF_KEY = 'def';
+
+ let origGon;
+ let origGl;
+
+ beforeEach(() => {
+ origGon = window.gon;
+ origGl = window.gl;
+ window.gon.experiment = {};
+ window.gl.experiments = {};
+ });
+
+ afterEach(() => {
+ window.gon = origGon;
+ window.gl = origGl;
+ });
describe('getExperimentData', () => {
+ const ABC_DATA = '_abc_data_';
+ const ABC_DATA2 = '_updated_abc_data_';
+ const DEF_DATA = '_def_data_';
+
describe.each`
- gon | input | output
- ${[TEST_KEY, '_data_']} | ${[TEST_KEY]} | ${{ variant: '_data_' }}
- ${[]} | ${[TEST_KEY]} | ${undefined}
- `('with input=$input and gon=$gon', ({ gon, input, output }) => {
- assignGitlabExperiment(...gon);
+ gonData | glData | input | output
+ ${[ABC_KEY, ABC_DATA]} | ${[]} | ${[ABC_KEY]} | ${{ experiment: ABC_KEY, variant: ABC_DATA }}
+ ${[]} | ${[ABC_KEY, ABC_DATA]} | ${[ABC_KEY]} | ${{ experiment: ABC_KEY, variant: ABC_DATA }}
+ ${[ABC_KEY, ABC_DATA]} | ${[DEF_KEY, DEF_DATA]} | ${[ABC_KEY]} | ${{ experiment: ABC_KEY, variant: ABC_DATA }}
+ ${[ABC_KEY, ABC_DATA]} | ${[DEF_KEY, DEF_DATA]} | ${[DEF_KEY]} | ${{ experiment: DEF_KEY, variant: DEF_DATA }}
+ ${[ABC_KEY, ABC_DATA]} | ${[ABC_KEY, ABC_DATA2]} | ${[ABC_KEY]} | ${{ experiment: ABC_KEY, variant: ABC_DATA2 }}
+ ${[]} | ${[]} | ${[ABC_KEY]} | ${undefined}
+ `('with input=$input, gon=$gonData, & gl=$glData', ({ gonData, glData, input, output }) => {
+ beforeEach(() => {
+ const [gonKey, gonVariant] = gonData;
+ const [glKey, glVariant] = glData;
+
+ if (gonKey) window.gon.experiment[gonKey] = { experiment: gonKey, variant: gonVariant };
+ if (glKey) window.gl.experiments[glKey] = { experiment: glKey, variant: glVariant };
+ });
it(`returns ${output}`, () => {
expect(experimentUtils.getExperimentData(...input)).toEqual(output);
@@ -25,106 +55,129 @@ describe('experiment Utilities', () => {
describe('getAllExperimentContexts', () => {
const schema = TRACKING_CONTEXT_SCHEMA;
- let origGon;
-
- beforeEach(() => {
- origGon = window.gon;
- });
-
- afterEach(() => {
- window.gon = origGon;
- });
it('collects all of the experiment contexts into a single array', () => {
- const experiments = [
- { experiment: 'abc', variant: 'candidate' },
- { experiment: 'def', variant: 'control' },
- { experiment: 'ghi', variant: 'blue' },
- ];
- window.gon = {
- experiment: experiments.reduce((collector, { experiment, variant }) => {
- return { ...collector, [experiment]: { experiment, variant } };
- }, {}),
- };
+ const experiments = { [ABC_KEY]: 'candidate', [DEF_KEY]: 'control', ghi: 'blue' };
+
+ stubExperiments(experiments);
expect(experimentUtils.getAllExperimentContexts()).toEqual(
- experiments.map((data) => ({ schema, data })),
+ Object.entries(experiments).map(([experiment, variant]) => ({
+ schema,
+ data: { experiment, variant },
+ })),
);
});
it('returns an empty array if there are no experiments', () => {
- window.gon.experiment = {};
-
expect(experimentUtils.getAllExperimentContexts()).toEqual([]);
});
- it('includes all additional experiment data', () => {
- const experiment = 'experimentWithCustomData';
- const data = { experiment, variant: 'control', color: 'blue', style: 'rounded' };
- window.gon.experiment[experiment] = data;
+ it('only collects the data properties which are supported by the schema', () => {
+ origGl = window.gl;
+ window.gl.experiments = {
+ my_experiment: { experiment: 'my_experiment', variant: 'control', excluded: false },
+ };
+
+ expect(experimentUtils.getAllExperimentContexts()).toEqual([
+ { schema, data: { experiment: 'my_experiment', variant: 'control' } },
+ ]);
- expect(experimentUtils.getAllExperimentContexts()).toContainEqual({ schema, data });
+ window.gl = origGl;
});
});
describe('isExperimentVariant', () => {
describe.each`
- gon | input | output
- ${[TEST_KEY, DEFAULT_VARIANT]} | ${[TEST_KEY, DEFAULT_VARIANT]} | ${true}
- ${[TEST_KEY, '_variant_name']} | ${[TEST_KEY, '_variant_name']} | ${true}
- ${[TEST_KEY, '_variant_name']} | ${[TEST_KEY, '_bogus_name']} | ${false}
- ${[TEST_KEY, '_variant_name']} | ${['boguskey', '_variant_name']} | ${false}
- ${[]} | ${[TEST_KEY, '_variant_name']} | ${false}
- `('with input=$input and gon=$gon', ({ gon, input, output }) => {
- assignGitlabExperiment(...gon);
-
- it(`returns ${output}`, () => {
- expect(experimentUtils.isExperimentVariant(...input)).toEqual(output);
- });
- });
+ experiment | variant | input | output
+ ${ABC_KEY} | ${DEFAULT_VARIANT} | ${[ABC_KEY, DEFAULT_VARIANT]} | ${true}
+ ${ABC_KEY} | ${'_variant_name'} | ${[ABC_KEY, '_variant_name']} | ${true}
+ ${ABC_KEY} | ${'_variant_name'} | ${[ABC_KEY, '_bogus_name']} | ${false}
+ ${ABC_KEY} | ${'_variant_name'} | ${['boguskey', '_variant_name']} | ${false}
+ ${undefined} | ${undefined} | ${[ABC_KEY, '_variant_name']} | ${false}
+ `(
+ 'with input=$input, experiment=$experiment, variant=$variant',
+ ({ experiment, variant, input, output }) => {
+ it(`returns ${output}`, () => {
+ if (experiment) stubExperiments({ [experiment]: variant });
+
+ expect(experimentUtils.isExperimentVariant(...input)).toEqual(output);
+ });
+ },
+ );
});
describe('experiment', () => {
+ const experiment = 'marley';
+ const useSpy = jest.fn();
const controlSpy = jest.fn();
+ const trySpy = jest.fn();
const candidateSpy = jest.fn();
const getUpStandUpSpy = jest.fn();
const variants = {
- use: controlSpy,
- try: candidateSpy,
+ use: useSpy,
+ try: trySpy,
get_up_stand_up: getUpStandUpSpy,
};
describe('when there is no experiment data', () => {
- it('calls control variant', () => {
- experimentUtils.experiment('marley', variants);
- expect(controlSpy).toHaveBeenCalled();
+ it('calls the use variant', () => {
+ experimentUtils.experiment(experiment, variants);
+ expect(useSpy).toHaveBeenCalled();
+ });
+
+ describe("when 'control' is provided instead of 'use'", () => {
+ it('calls the control variant', () => {
+ experimentUtils.experiment(experiment, { control: controlSpy });
+ expect(controlSpy).toHaveBeenCalled();
+ });
});
});
describe('when experiment variant is "control"', () => {
- assignGitlabExperiment('marley', DEFAULT_VARIANT);
+ beforeEach(() => {
+ stubExperiments({ [experiment]: DEFAULT_VARIANT });
+ });
- it('calls the control variant', () => {
- experimentUtils.experiment('marley', variants);
- expect(controlSpy).toHaveBeenCalled();
+ it('calls the use variant', () => {
+ experimentUtils.experiment(experiment, variants);
+ expect(useSpy).toHaveBeenCalled();
+ });
+
+ describe("when 'control' is provided instead of 'use'", () => {
+ it('calls the control variant', () => {
+ experimentUtils.experiment(experiment, { control: controlSpy });
+ expect(controlSpy).toHaveBeenCalled();
+ });
});
});
describe('when experiment variant is "candidate"', () => {
- assignGitlabExperiment('marley', CANDIDATE_VARIANT);
+ beforeEach(() => {
+ stubExperiments({ [experiment]: CANDIDATE_VARIANT });
+ });
- it('calls the candidate variant', () => {
- experimentUtils.experiment('marley', variants);
- expect(candidateSpy).toHaveBeenCalled();
+ it('calls the try variant', () => {
+ experimentUtils.experiment(experiment, variants);
+ expect(trySpy).toHaveBeenCalled();
+ });
+
+ describe("when 'candidate' is provided instead of 'try'", () => {
+ it('calls the candidate variant', () => {
+ experimentUtils.experiment(experiment, { candidate: candidateSpy });
+ expect(candidateSpy).toHaveBeenCalled();
+ });
});
});
describe('when experiment variant is "get_up_stand_up"', () => {
- assignGitlabExperiment('marley', 'get_up_stand_up');
+ beforeEach(() => {
+ stubExperiments({ [experiment]: 'get_up_stand_up' });
+ });
it('calls the get-up-stand-up variant', () => {
- experimentUtils.experiment('marley', variants);
+ experimentUtils.experiment(experiment, variants);
expect(getUpStandUpSpy).toHaveBeenCalled();
});
});
@@ -132,14 +185,17 @@ describe('experiment Utilities', () => {
describe('getExperimentVariant', () => {
it.each`
- gon | input | output
- ${{ experiment: { [TEST_KEY]: { variant: DEFAULT_VARIANT } } }} | ${[TEST_KEY]} | ${DEFAULT_VARIANT}
- ${{ experiment: { [TEST_KEY]: { variant: CANDIDATE_VARIANT } } }} | ${[TEST_KEY]} | ${CANDIDATE_VARIANT}
- ${{}} | ${[TEST_KEY]} | ${DEFAULT_VARIANT}
- `('with input=$input and gon=$gon, returns $output', ({ gon, input, output }) => {
- window.gon = gon;
-
- expect(experimentUtils.getExperimentVariant(...input)).toEqual(output);
- });
+ experiment | variant | input | output
+ ${ABC_KEY} | ${DEFAULT_VARIANT} | ${ABC_KEY} | ${DEFAULT_VARIANT}
+ ${ABC_KEY} | ${CANDIDATE_VARIANT} | ${ABC_KEY} | ${CANDIDATE_VARIANT}
+ ${undefined} | ${undefined} | ${ABC_KEY} | ${DEFAULT_VARIANT}
+ `(
+ 'with input=$input, experiment=$experiment, & variant=$variant; returns $output',
+ ({ experiment, variant, input, output }) => {
+ stubExperiments({ [experiment]: variant });
+
+ expect(experimentUtils.getExperimentVariant(input)).toEqual(output);
+ },
+ );
});
});
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 27ec6a7280f..f244da228b3 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,6 @@
import { GlModal, GlSprintf, GlAlert } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import Component from '~/feature_flags/components/configure_feature_flags_modal.vue';
describe('Configure Feature Flags Modal', () => {
@@ -20,7 +21,7 @@ describe('Configure Feature Flags Modal', () => {
};
let wrapper;
- const factory = (props = {}, { mountFn = shallowMount, ...options } = {}) => {
+ const factory = (props = {}, { mountFn = shallowMountExtended, ...options } = {}) => {
wrapper = mountFn(Component, {
provide,
stubs: { GlSprintf },
@@ -140,11 +141,13 @@ describe('Configure Feature Flags Modal', () => {
describe('has rotate error', () => {
afterEach(() => wrapper.destroy());
- beforeEach(factory.bind(null, { hasRotateError: false }));
+ beforeEach(() => {
+ factory({ hasRotateError: true });
+ });
it('should display an error', async () => {
- expect(wrapper.find('.text-danger')).toExist();
- expect(wrapper.find('[name="warning"]')).toExist();
+ expect(wrapper.findByTestId('rotate-error').exists()).toBe(true);
+ expect(wrapper.find('[name="warning"]').exists()).toBe(true);
});
});
diff --git a/spec/frontend/filterable_list_spec.js b/spec/frontend/filterable_list_spec.js
index 556cf6f8137..3fd5d198e3a 100644
--- a/spec/frontend/filterable_list_spec.js
+++ b/spec/frontend/filterable_list_spec.js
@@ -1,5 +1,4 @@
-// eslint-disable-next-line import/no-deprecated
-import { getJSONFixture, setHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture } from 'helpers/fixtures';
import FilterableList from '~/filterable_list';
describe('FilterableList', () => {
@@ -15,8 +14,6 @@ describe('FilterableList', () => {
</div>
<div class="js-projects-list-holder"></div>
`);
- // eslint-disable-next-line import/no-deprecated
- getJSONFixture('static/projects.json');
form = document.querySelector('form#project-filter-form');
filter = document.querySelector('.js-projects-list-filter');
holder = document.querySelector('.js-projects-list-holder');
diff --git a/spec/frontend/fixtures/api_markdown.yml b/spec/frontend/fixtures/api_markdown.yml
index 45f73260887..8fd6a5531db 100644
--- a/spec/frontend/fixtures/api_markdown.yml
+++ b/spec/frontend/fixtures/api_markdown.yml
@@ -2,63 +2,75 @@
# spec/frontend/fixtures/api_markdown.rb and
# spec/frontend/content_editor/extensions/markdown_processing_spec.js
---
+- name: attachment_image
+ context: group
+ markdown: '![test-file](/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.png)'
+- name: attachment_image
+ context: project
+ markdown: '![test-file](/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.png)'
+- name: attachment_image
+ context: project_wiki
+ markdown: '![test-file](test-file.png)'
+- name: attachment_link
+ context: group
+ markdown: '[test-file](/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.zip)'
+- name: attachment_link
+ context: project
+ markdown: '[test-file](/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.zip)'
+- name: attachment_link
+ context: project_wiki
+ markdown: '[test-file](test-file.zip)'
+- name: audio
+ markdown: '![Sample Audio](https://gitlab.com/gitlab.mp3)'
+- name: audio_and_video_in_lists
+ markdown: |-
+ * ![Sample Audio](https://gitlab.com/1.mp3)
+ * ![Sample Video](https://gitlab.com/2.mp4)
+
+ 1. ![Sample Video](https://gitlab.com/1.mp4)
+ 2. ![Sample Audio](https://gitlab.com/2.mp3)
+
+ * [x] ![Sample Audio](https://gitlab.com/1.mp3)
+ * [x] ![Sample Audio](https://gitlab.com/2.mp3)
+ * [x] ![Sample Video](https://gitlab.com/3.mp4)
+- name: blockquote
+ markdown: |-
+ > This is a blockquote
+ >
+ > This is another one
- name: bold
markdown: '**bold**'
-- name: emphasis
- markdown: '_emphasized text_'
-- name: inline_code
- markdown: '`code`'
-- name: inline_diff
+- name: bullet_list_style_1
markdown: |-
- * {-deleted-}
- * {+added+}
-- name: strike
- markdown: '~~del~~'
-- name: horizontal_rule
- markdown: '---'
-- name: html_marks
+ * list item 1
+ * list item 2
+ * embedded list item 3
+- name: bullet_list_style_2
markdown: |-
- * Content editor is ~~great~~<ins>amazing</ins>.
- * If the changes <abbr title="Looks good to merge">LGTM</abbr>, please <abbr title="Merge when pipeline succeeds">MWPS</abbr>.
- * The English song <q>Oh I do like to be beside the seaside</q> looks like this in Hebrew: <span dir="rtl">אה, אני אוהב להיות ליד חוף הים</span>. In the computer's memory, this is stored as <bdo dir="ltr">אה, אני אוהב להיות ליד חוף הים</bdo>.
- * <cite>The Scream</cite> by Edvard Munch. Painted in 1893.
- * <dfn>HTML</dfn> is the standard markup language for creating web pages.
- * Do not forget to buy <mark>milk</mark> today.
- * This is a paragraph and <small>smaller text goes here</small>.
- * The concert starts at <time datetime="20:00">20:00</time> and you'll be able to enjoy the band for at least <time datetime="PT2H30M">2h 30m</time>.
- * Press <kbd>Ctrl</kbd> + <kbd>C</kbd> to copy text (Windows).
- * WWF's goal is to: <q>Build a future where people live in harmony with nature.</q> We hope they succeed.
- * The error occured was: <samp>Keyboard not found. Press F1 to continue.</samp>
- * The area of a triangle is: 1/2 x <var>b</var> x <var>h</var>, where <var>b</var> is the base, and <var>h</var> is the vertical height.
- * <ruby>漢<rt>ㄏㄢˋ</rt></ruby>
- * C<sub>7</sub>H<sub>16</sub> + O<sub>2</sub> → CO<sub>2</sub> + H<sub>2</sub>O
- * The **Pythagorean theorem** is often expressed as <var>a<sup>2</sup></var> + <var>b<sup>2</sup></var> = <var>c<sup>2</sup></var>
-- name: div
+ - list item 1
+ - list item 2
+ * embedded list item 3
+- name: bullet_list_style_3
markdown: |-
- <div>plain text</div>
- <div>
-
- just a plain ol' div, not much to _expect_!
-
- </div>
-- name: figure
+ + list item 1
+ + list item 2
+ - embedded list item 3
+- name: code_block
markdown: |-
- <figure>
-
- ![Elephant at sunset](elephant-sunset.jpg)
-
- <figcaption>An elephant at sunset</figcaption>
- </figure>
- <figure>
-
- ![A crocodile wearing crocs](croc-crocs.jpg)
-
- <figcaption>
-
- A crocodile wearing _crocs_!
-
- </figcaption>
- </figure>
+ ```javascript
+ console.log('hello world')
+ ```
+- name: color_chips
+ markdown: |-
+ - `#F00`
+ - `#F00A`
+ - `#FF0000`
+ - `#FF0000AA`
+ - `RGB(0,255,0)`
+ - `RGB(0%,100%,0%)`
+ - `RGBA(0,255,0,0.3)`
+ - `HSL(540,70%,50%)`
+ - `HSLA(540,70%,50%,0.3)`
- name: description_list
markdown: |-
<dl>
@@ -106,31 +118,57 @@
```
</details>
-- name: link
- markdown: '[GitLab](https://gitlab.com)'
-- name: attachment_link
- context: project_wiki
- markdown: '[test-file](test-file.zip)'
-- name: attachment_link
- context: project
- markdown: '[test-file](/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.zip)'
-- name: attachment_link
- context: group
- markdown: '[test-file](/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.zip)'
-- name: attachment_image
- context: project_wiki
- markdown: '![test-file](test-file.png)'
-- name: attachment_image
- context: project
- markdown: '![test-file](/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.png)'
-- name: attachment_image
- context: group
- markdown: '![test-file](/uploads/aa45a38ec2cfe97433281b10bbff042c/test-file.png)'
-- name: code_block
+- name: div
markdown: |-
- ```javascript
- console.log('hello world')
- ```
+ <div>plain text</div>
+ <div>
+
+ just a plain ol' div, not much to _expect_!
+
+ </div>
+- name: emoji
+ markdown: ':sparkles: :heart: :100:'
+- name: emphasis
+ markdown: '_emphasized text_'
+- name: figure
+ markdown: |-
+ <figure>
+
+ ![Elephant at sunset](elephant-sunset.jpg)
+
+ <figcaption>An elephant at sunset</figcaption>
+ </figure>
+ <figure>
+
+ ![A crocodile wearing crocs](croc-crocs.jpg)
+
+ <figcaption>
+
+ A crocodile wearing _crocs_!
+
+ </figcaption>
+ </figure>
+- name: frontmatter_json
+ markdown: |-
+ ;;;
+ {
+ "title": "Page title"
+ }
+ ;;;
+- name: frontmatter_toml
+ markdown: |-
+ +++
+ title = "Page title"
+ +++
+- name: frontmatter_yaml
+ markdown: |-
+ ---
+ title: Page title
+ ---
+- name: hard_break
+ markdown: |-
+ This is a line after a\
+ hard break
- name: headings
markdown: |-
# Heading 1
@@ -144,29 +182,44 @@
##### Heading 5
###### Heading 6
-- name: blockquote
- markdown: |-
- > This is a blockquote
- >
- > This is another one
-- name: thematic_break
- markdown: |-
- ---
-- name: bullet_list_style_1
+- name: horizontal_rule
+ markdown: '---'
+- name: html_marks
markdown: |-
- * list item 1
- * list item 2
- * embedded list item 3
-- name: bullet_list_style_2
+ * Content editor is ~~great~~<ins>amazing</ins>.
+ * If the changes <abbr title="Looks good to merge">LGTM</abbr>, please <abbr title="Merge when pipeline succeeds">MWPS</abbr>.
+ * The English song <q>Oh I do like to be beside the seaside</q> looks like this in Hebrew: <span dir="rtl">אה, אני אוהב להיות ליד חוף הים</span>. In the computer's memory, this is stored as <bdo dir="ltr">אה, אני אוהב להיות ליד חוף הים</bdo>.
+ * <cite>The Scream</cite> by Edvard Munch. Painted in 1893.
+ * <dfn>HTML</dfn> is the standard markup language for creating web pages.
+ * Do not forget to buy <mark>milk</mark> today.
+ * This is a paragraph and <small>smaller text goes here</small>.
+ * The concert starts at <time datetime="20:00">20:00</time> and you'll be able to enjoy the band for at least <time datetime="PT2H30M">2h 30m</time>.
+ * Press <kbd>Ctrl</kbd> + <kbd>C</kbd> to copy text (Windows).
+ * WWF's goal is to: <q>Build a future where people live in harmony with nature.</q> We hope they succeed.
+ * The error occured was: <samp>Keyboard not found. Press F1 to continue.</samp>
+ * The area of a triangle is: 1/2 x <var>b</var> x <var>h</var>, where <var>b</var> is the base, and <var>h</var> is the vertical height.
+ * <ruby>漢<rt>ㄏㄢˋ</rt></ruby>
+ * C<sub>7</sub>H<sub>16</sub> + O<sub>2</sub> → CO<sub>2</sub> + H<sub>2</sub>O
+ * The **Pythagorean theorem** is often expressed as <var>a<sup>2</sup></var> + <var>b<sup>2</sup></var> = <var>c<sup>2</sup></var>
+- name: image
+ markdown: '![alt text](https://gitlab.com/logo.png)'
+- name: inline_code
+ markdown: '`code`'
+- name: inline_diff
markdown: |-
- - list item 1
- - list item 2
- * embedded list item 3
-- name: bullet_list_style_3
+ * {-deleted-}
+ * {+added+}
+- name: link
+ markdown: '[GitLab](https://gitlab.com)'
+- name: math
markdown: |-
- + list item 1
- + list item 2
- - embedded list item 3
+ This math is inline $`a^2+b^2=c^2`$.
+
+ This is on a separate line:
+
+ ```math
+ a^2+b^2=c^2
+ ```
- name: ordered_list
markdown: |-
1. list item 1
@@ -177,14 +230,6 @@
134. list item 1
135. list item 2
136. list item 3
-- name: task_list
- markdown: |-
- * [x] hello
- * [x] world
- * [ ] example
- * [ ] of nested
- * [x] task list
- * [ ] items
- name: ordered_task_list
markdown: |-
1. [x] hello
@@ -198,12 +243,12 @@
4893. [x] hello
4894. [x] world
4895. [ ] example
-- name: image
- markdown: '![alt text](https://gitlab.com/logo.png)'
-- name: hard_break
+- name: reference
+ context: project_wiki
markdown: |-
- This is a line after a\
- hard break
+ Hi @gitlab - thank you for reporting this ~bug (#1) we hope to fix it in %1.1 as part of !1
+- name: strike
+ markdown: '~~del~~'
- name: table
markdown: |-
| header | header |
@@ -212,27 +257,6 @@
| ~~strike~~ | cell with _italic_ |
# content after table
-- name: emoji
- markdown: ':sparkles: :heart: :100:'
-- name: reference
- context: project_wiki
- markdown: |-
- Hi @gitlab - thank you for reporting this ~bug (#1) we hope to fix it in %1.1 as part of !1
-- name: audio
- markdown: '![Sample Audio](https://gitlab.com/gitlab.mp3)'
-- name: video
- markdown: '![Sample Video](https://gitlab.com/gitlab.mp4)'
-- name: audio_and_video_in_lists
- markdown: |-
- * ![Sample Audio](https://gitlab.com/1.mp3)
- * ![Sample Video](https://gitlab.com/2.mp4)
-
- 1. ![Sample Video](https://gitlab.com/1.mp4)
- 2. ![Sample Audio](https://gitlab.com/2.mp3)
-
- * [x] ![Sample Audio](https://gitlab.com/1.mp3)
- * [x] ![Sample Audio](https://gitlab.com/2.mp3)
- * [x] ![Sample Video](https://gitlab.com/3.mp4)
- name: table_of_contents
markdown: |-
[[_TOC_]]
@@ -248,42 +272,18 @@
# Sit amit
### I don't know
-- name: word_break
- markdown: Fernstraßen<wbr>bau<wbr>privat<wbr>finanzierungs<wbr>gesetz
-- name: frontmatter_yaml
- markdown: |-
- ---
- title: Page title
- ---
-- name: frontmatter_toml
- markdown: |-
- +++
- title = "Page title"
- +++
-- name: frontmatter_json
- markdown: |-
- ;;;
- {
- "title": "Page title"
- }
- ;;;
-- name: color_chips
+- name: task_list
markdown: |-
- - `#F00`
- - `#F00A`
- - `#FF0000`
- - `#FF0000AA`
- - `RGB(0,255,0)`
- - `RGB(0%,100%,0%)`
- - `RGBA(0,255,0,0.3)`
- - `HSL(540,70%,50%)`
- - `HSLA(540,70%,50%,0.3)`
-- name: math
+ * [x] hello
+ * [x] world
+ * [ ] example
+ * [ ] of nested
+ * [x] task list
+ * [ ] items
+- name: thematic_break
markdown: |-
- This math is inline $`a^2+b^2=c^2`$.
-
- This is on a separate line:
-
- ```math
- a^2+b^2=c^2
- ```
+ ---
+- name: video
+ markdown: '![Sample Video](https://gitlab.com/gitlab.mp4)'
+- name: word_break
+ markdown: Fernstraßen<wbr>bau<wbr>privat<wbr>finanzierungs<wbr>gesetz
diff --git a/spec/frontend/fixtures/projects.rb b/spec/frontend/fixtures/projects.rb
index 3c8964d398a..23c18c97df2 100644
--- a/spec/frontend/fixtures/projects.rb
+++ b/spec/frontend/fixtures/projects.rb
@@ -65,5 +65,31 @@ RSpec.describe 'Projects (JavaScript fixtures)', type: :controller do
expect_graphql_errors_to_be_empty
end
end
+
+ context 'project storage count query' do
+ before do
+ project.statistics.update!(
+ repository_size: 3900000,
+ lfs_objects_size: 4800000,
+ build_artifacts_size: 400000,
+ pipeline_artifacts_size: 400000,
+ wiki_size: 300000,
+ packages_size: 3800000,
+ uploads_size: 900000
+ )
+ end
+
+ base_input_path = 'projects/storage_counter/queries/'
+ base_output_path = 'graphql/projects/storage_counter/'
+ query_name = 'project_storage.query.graphql'
+
+ it "#{base_output_path}#{query_name}.json" do
+ query = get_graphql_query_as_string("#{base_input_path}#{query_name}")
+
+ post_graphql(query, current_user: user, variables: { fullPath: project.full_path })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
end
end
diff --git a/spec/frontend/flash_spec.js b/spec/frontend/flash_spec.js
index 96e5202780b..f7bde8d2f16 100644
--- a/spec/frontend/flash_spec.js
+++ b/spec/frontend/flash_spec.js
@@ -3,6 +3,7 @@ import createFlash, {
createAction,
hideFlash,
removeFlashClickListener,
+ FLASH_CLOSED_EVENT,
} from '~/flash';
describe('Flash', () => {
@@ -79,6 +80,16 @@ describe('Flash', () => {
expect(el.remove.mock.calls.length).toBe(1);
});
+
+ it(`dispatches ${FLASH_CLOSED_EVENT} event after transitionend event`, () => {
+ jest.spyOn(el, 'dispatchEvent');
+
+ hideFlash(el);
+
+ el.dispatchEvent(new Event('transitionend'));
+
+ expect(el.dispatchEvent).toHaveBeenCalledWith(new Event(FLASH_CLOSED_EVENT));
+ });
});
describe('createAction', () => {
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index eb11df2fe43..631e3307f7f 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -2,7 +2,7 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import labelsFixture from 'test_fixtures/autocomplete_sources/labels.json';
-import GfmAutoComplete, { membersBeforeSave } from 'ee_else_ce/gfm_auto_complete';
+import GfmAutoComplete, { membersBeforeSave, highlighter } from 'ee_else_ce/gfm_auto_complete';
import { initEmojiMock } from 'helpers/emoji';
import '~/lib/utils/jquery_at_who';
import { TEST_HOST } from 'helpers/test_constants';
@@ -858,4 +858,14 @@ describe('GfmAutoComplete', () => {
);
});
});
+
+ describe('highlighter', () => {
+ it('escapes regex', () => {
+ const li = '<li>couple (woman,woman) <gl-emoji data-name="couple_ww"></gl-emoji></li>';
+
+ expect(highlighter(li, ')')).toBe(
+ '<li> couple (woman,woman<strong>)</strong> <gl-emoji data-name="couple_ww"></gl-emoji></li>',
+ );
+ });
+ });
});
diff --git a/spec/frontend/google_cloud/components/app_spec.js b/spec/frontend/google_cloud/components/app_spec.js
new file mode 100644
index 00000000000..bb86eb5c22e
--- /dev/null
+++ b/spec/frontend/google_cloud/components/app_spec.js
@@ -0,0 +1,66 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlTab, GlTabs } from '@gitlab/ui';
+import App from '~/google_cloud/components/app.vue';
+import IncubationBanner from '~/google_cloud/components/incubation_banner.vue';
+import ServiceAccounts from '~/google_cloud/components/service_accounts.vue';
+
+describe('google_cloud App component', () => {
+ let wrapper;
+
+ const findIncubationBanner = () => wrapper.findComponent(IncubationBanner);
+ const findTabs = () => wrapper.findComponent(GlTabs);
+ const findTabItems = () => findTabs().findAllComponents(GlTab);
+ const findConfigurationTab = () => findTabItems().at(0);
+ const findDeploymentTab = () => findTabItems().at(1);
+ const findServicesTab = () => findTabItems().at(2);
+ const findServiceAccounts = () => findConfigurationTab().findComponent(ServiceAccounts);
+
+ beforeEach(() => {
+ const propsData = {
+ serviceAccounts: [{}, {}],
+ createServiceAccountUrl: '#url-create-service-account',
+ emptyIllustrationUrl: '#url-empty-illustration',
+ };
+ wrapper = shallowMount(App, { propsData });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('should contain incubation banner', () => {
+ expect(findIncubationBanner().exists()).toBe(true);
+ });
+
+ describe('google_cloud App tabs', () => {
+ it('should contain tabs', () => {
+ expect(findTabs().exists()).toBe(true);
+ });
+
+ it('should contain three tab items', () => {
+ expect(findTabItems().length).toBe(3);
+ });
+
+ describe('configuration tab', () => {
+ it('should exist', () => {
+ expect(findConfigurationTab().exists()).toBe(true);
+ });
+
+ it('should contain service accounts component', () => {
+ expect(findServiceAccounts().exists()).toBe(true);
+ });
+ });
+
+ describe('deployments tab', () => {
+ it('should exist', () => {
+ expect(findDeploymentTab().exists()).toBe(true);
+ });
+ });
+
+ describe('services tab', () => {
+ it('should exist', () => {
+ expect(findServicesTab().exists()).toBe(true);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/google_cloud/components/incubation_banner_spec.js b/spec/frontend/google_cloud/components/incubation_banner_spec.js
new file mode 100644
index 00000000000..89517be4ef1
--- /dev/null
+++ b/spec/frontend/google_cloud/components/incubation_banner_spec.js
@@ -0,0 +1,60 @@
+import { mount } from '@vue/test-utils';
+import { GlAlert, GlLink } from '@gitlab/ui';
+import IncubationBanner from '~/google_cloud/components/incubation_banner.vue';
+
+describe('IncubationBanner component', () => {
+ let wrapper;
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findLinks = () => wrapper.findAllComponents(GlLink);
+ const findFeatureRequestLink = () => findLinks().at(0);
+ const findReportBugLink = () => findLinks().at(1);
+ const findShareFeedbackLink = () => findLinks().at(2);
+
+ beforeEach(() => {
+ const propsData = {
+ shareFeedbackUrl: 'url_general_feedback',
+ reportBugUrl: 'url_report_bug',
+ featureRequestUrl: 'url_feature_request',
+ };
+ wrapper = mount(IncubationBanner, { propsData });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('contains alert', () => {
+ expect(findAlert().exists()).toBe(true);
+ });
+
+ it('contains relevant text', () => {
+ expect(findAlert().text()).toContain(
+ 'This is an experimental feature developed by GitLab Incubation Engineering.',
+ );
+ });
+
+ describe('has relevant gl-links', () => {
+ it('three in total', () => {
+ expect(findLinks().length).toBe(3);
+ });
+
+ it('contains feature request link', () => {
+ const link = findFeatureRequestLink();
+ expect(link.text()).toBe('request a feature');
+ expect(link.attributes('href')).toBe('url_feature_request');
+ });
+
+ it('contains report bug link', () => {
+ const link = findReportBugLink();
+ expect(link.text()).toBe('report a bug');
+ expect(link.attributes('href')).toBe('url_report_bug');
+ });
+
+ it('contains share feedback link', () => {
+ const link = findShareFeedbackLink();
+ expect(link.text()).toBe('share feedback');
+ expect(link.attributes('href')).toBe('url_general_feedback');
+ });
+ });
+});
diff --git a/spec/frontend/google_cloud/components/service_accounts_spec.js b/spec/frontend/google_cloud/components/service_accounts_spec.js
new file mode 100644
index 00000000000..3d097078f03
--- /dev/null
+++ b/spec/frontend/google_cloud/components/service_accounts_spec.js
@@ -0,0 +1,79 @@
+import { mount } from '@vue/test-utils';
+import { GlButton, GlEmptyState, GlTable } from '@gitlab/ui';
+import ServiceAccounts from '~/google_cloud/components/service_accounts.vue';
+
+describe('ServiceAccounts component', () => {
+ describe('when the project does not have any service accounts', () => {
+ let wrapper;
+
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findButtonInEmptyState = () => findEmptyState().findComponent(GlButton);
+
+ beforeEach(() => {
+ const propsData = {
+ list: [],
+ createUrl: '#create-url',
+ emptyIllustrationUrl: '#empty-illustration-url',
+ };
+ wrapper = mount(ServiceAccounts, { propsData });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('shows the empty state component', () => {
+ expect(findEmptyState().exists()).toBe(true);
+ });
+ it('shows the link to create new service accounts', () => {
+ const button = findButtonInEmptyState();
+ expect(button.exists()).toBe(true);
+ expect(button.text()).toBe('Create service account');
+ expect(button.attributes('href')).toBe('#create-url');
+ });
+ });
+
+ describe('when three service accounts are passed via props', () => {
+ let wrapper;
+
+ const findTitle = () => wrapper.find('h2');
+ const findDescription = () => wrapper.find('p');
+ const findTable = () => wrapper.findComponent(GlTable);
+ const findRows = () => findTable().findAll('tr');
+ const findButton = () => wrapper.findComponent(GlButton);
+
+ beforeEach(() => {
+ const propsData = {
+ list: [{}, {}, {}],
+ createUrl: '#create-url',
+ emptyIllustrationUrl: '#empty-illustration-url',
+ };
+ wrapper = mount(ServiceAccounts, { propsData });
+ });
+
+ it('shows the title', () => {
+ expect(findTitle().text()).toBe('Service Accounts');
+ });
+
+ it('shows the description', () => {
+ expect(findDescription().text()).toBe(
+ 'Service Accounts keys authorize GitLab to deploy your Google Cloud project',
+ );
+ });
+
+ it('shows the table', () => {
+ expect(findTable().exists()).toBe(true);
+ });
+
+ it('table must have three rows + header row', () => {
+ expect(findRows().length).toBe(4);
+ });
+
+ it('shows the link to create new service accounts', () => {
+ const button = findButton();
+ expect(button.exists()).toBe(true);
+ expect(button.text()).toBe('Create service account');
+ expect(button.attributes('href')).toBe('#create-url');
+ });
+ });
+});
diff --git a/spec/frontend/graphql_shared/utils_spec.js b/spec/frontend/graphql_shared/utils_spec.js
index 1732f24eeff..9f478eedbfb 100644
--- a/spec/frontend/graphql_shared/utils_spec.js
+++ b/spec/frontend/graphql_shared/utils_spec.js
@@ -52,6 +52,10 @@ describe('getIdFromGraphQLId', () => {
output: null,
},
{
+ input: 'gid://gitlab/Environments/0',
+ output: 0,
+ },
+ {
input: 'gid://gitlab/Environments/123',
output: 123,
},
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 78950a8fe20..617d91178e4 100644
--- a/spec/frontend/group_settings/components/shared_runners_form_spec.js
+++ b/spec/frontend/group_settings/components/shared_runners_form_spec.js
@@ -3,13 +3,16 @@ 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';
-import { ENABLED, DISABLED, ALLOW_OVERRIDE } from '~/group_settings/constants';
import axios from '~/lib/utils/axios_utils';
-const TEST_UPDATE_PATH = '/test/update';
-const DISABLED_PAYLOAD = { shared_runners_setting: DISABLED };
-const ENABLED_PAYLOAD = { shared_runners_setting: ENABLED };
-const OVERRIDE_PAYLOAD = { shared_runners_setting: ALLOW_OVERRIDE };
+const provide = {
+ updatePath: '/test/update',
+ sharedRunnersAvailability: 'enabled',
+ parentSharedRunnersAvailability: null,
+ runnerDisabled: 'disabled',
+ runnerEnabled: 'enabled',
+ runnerAllowOverride: 'allow_override',
+};
jest.mock('~/flash');
@@ -17,13 +20,11 @@ describe('group_settings/components/shared_runners_form', () => {
let wrapper;
let mock;
- const createComponent = (props = {}) => {
+ const createComponent = (provides = {}) => {
wrapper = shallowMount(SharedRunnersForm, {
- propsData: {
- updatePath: TEST_UPDATE_PATH,
- sharedRunnersAvailability: ENABLED,
- parentSharedRunnersAvailability: null,
- ...props,
+ provide: {
+ ...provide,
+ ...provides,
},
});
};
@@ -33,13 +34,13 @@ describe('group_settings/components/shared_runners_form', () => {
const findEnabledToggle = () => wrapper.find('[data-testid="enable-runners-toggle"]');
const findOverrideToggle = () => wrapper.find('[data-testid="override-runners-toggle"]');
const changeToggle = (toggle) => toggle.vm.$emit('change', !toggle.props('value'));
- const getRequestPayload = () => JSON.parse(mock.history.put[0].data);
+ const getSharedRunnersSetting = () => JSON.parse(mock.history.put[0].data).shared_runners_setting;
const isLoadingIconVisible = () => findLoadingIcon().exists();
beforeEach(() => {
mock = new MockAxiosAdapter(axios);
- mock.onPut(TEST_UPDATE_PATH).reply(200);
+ mock.onPut(provide.updatePath).reply(200);
});
afterEach(() => {
@@ -95,7 +96,7 @@ describe('group_settings/components/shared_runners_form', () => {
await waitForPromises();
- expect(getRequestPayload()).toEqual(ENABLED_PAYLOAD);
+ expect(getSharedRunnersSetting()).toEqual(provide.runnerEnabled);
expect(findOverrideToggle().exists()).toBe(false);
});
@@ -104,14 +105,14 @@ describe('group_settings/components/shared_runners_form', () => {
await waitForPromises();
- expect(getRequestPayload()).toEqual(DISABLED_PAYLOAD);
+ expect(getSharedRunnersSetting()).toEqual(provide.runnerDisabled);
expect(findOverrideToggle().exists()).toBe(true);
});
});
describe('override toggle', () => {
beforeEach(() => {
- createComponent({ sharedRunnersAvailability: ALLOW_OVERRIDE });
+ createComponent({ sharedRunnersAvailability: provide.runnerAllowOverride });
});
it('enabling the override toggle sends correct payload', async () => {
@@ -119,7 +120,7 @@ describe('group_settings/components/shared_runners_form', () => {
await waitForPromises();
- expect(getRequestPayload()).toEqual(OVERRIDE_PAYLOAD);
+ expect(getSharedRunnersSetting()).toEqual(provide.runnerAllowOverride);
});
it('disabling the override toggle sends correct payload', async () => {
@@ -127,21 +128,21 @@ describe('group_settings/components/shared_runners_form', () => {
await waitForPromises();
- expect(getRequestPayload()).toEqual(DISABLED_PAYLOAD);
+ expect(getSharedRunnersSetting()).toEqual(provide.runnerDisabled);
});
});
describe('toggle disabled state', () => {
- it(`toggles are not disabled with setting ${DISABLED}`, () => {
- createComponent({ sharedRunnersAvailability: DISABLED });
+ it(`toggles are not disabled with setting ${provide.runnerDisabled}`, () => {
+ createComponent({ sharedRunnersAvailability: provide.runnerDisabled });
expect(findEnabledToggle().props('disabled')).toBe(false);
expect(findOverrideToggle().props('disabled')).toBe(false);
});
it('toggles are disabled', () => {
createComponent({
- sharedRunnersAvailability: DISABLED,
- parentSharedRunnersAvailability: DISABLED,
+ sharedRunnersAvailability: provide.runnerDisabled,
+ parentSharedRunnersAvailability: provide.runnerDisabled,
});
expect(findEnabledToggle().props('disabled')).toBe(true);
expect(findOverrideToggle().props('disabled')).toBe(true);
@@ -154,7 +155,7 @@ describe('group_settings/components/shared_runners_form', () => {
${{ error: 'Undefined error' }} | ${'Undefined error Refresh the page and try again.'}
`(`with error $errorObj`, ({ errorObj, message }) => {
beforeEach(async () => {
- mock.onPut(TEST_UPDATE_PATH).reply(500, errorObj);
+ mock.onPut(provide.updatePath).reply(500, errorObj);
createComponent();
changeToggle(findEnabledToggle());
diff --git a/spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap b/spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap
index 194a619c4aa..47e3a56e83d 100644
--- a/spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap
+++ b/spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap
@@ -8,7 +8,7 @@ exports[`IDE pipelines list when loaded renders empty state when no latestPipeli
<empty-state-stub
cansetci="true"
- class="mb-auto mt-auto"
+ class="gl-p-5"
emptystatesvgpath="http://test.host"
/>
</div>
diff --git a/spec/frontend/ide/components/shared/commit_message_field_spec.js b/spec/frontend/ide/components/shared/commit_message_field_spec.js
new file mode 100644
index 00000000000..f4f9b95b233
--- /dev/null
+++ b/spec/frontend/ide/components/shared/commit_message_field_spec.js
@@ -0,0 +1,149 @@
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import CommitMessageField from '~/ide/components/shared/commit_message_field.vue';
+
+const DEFAULT_PROPS = {
+ text: 'foo text',
+ placeholder: 'foo placeholder',
+};
+
+describe('CommitMessageField', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = extendedWrapper(
+ shallowMount(CommitMessageField, {
+ propsData: {
+ ...DEFAULT_PROPS,
+ ...props,
+ },
+ attachTo: document.body,
+ }),
+ );
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findTextArea = () => wrapper.find('textarea');
+ const findHighlights = () => wrapper.findByTestId('highlights');
+ const findHighlightsText = () => wrapper.findByTestId('highlights-text');
+ const findHighlightsMark = () => wrapper.findByTestId('highlights-mark');
+ const findHighlightsTexts = () => wrapper.findAllByTestId('highlights-text');
+ const findHighlightsMarks = () => wrapper.findAllByTestId('highlights-mark');
+
+ const fillText = async (text) => {
+ wrapper.setProps({ text });
+ await nextTick();
+ };
+
+ it('emits input event on input', () => {
+ const value = 'foo';
+
+ createComponent();
+ findTextArea().setValue(value);
+ expect(wrapper.emitted('input')[0][0]).toEqual(value);
+ });
+
+ describe('focus classes', () => {
+ beforeEach(async () => {
+ createComponent();
+ findTextArea().trigger('focus');
+ await nextTick();
+ });
+
+ it('is added on textarea focus', async () => {
+ expect(wrapper.attributes('class')).toEqual(
+ expect.stringContaining('gl-outline-none! gl-focus-ring-border-1-gray-900!'),
+ );
+ });
+
+ it('is removed on textarea blur', async () => {
+ findTextArea().trigger('blur');
+ await nextTick();
+
+ expect(wrapper.attributes('class')).toEqual(
+ expect.not.stringContaining('gl-outline-none! gl-focus-ring-border-1-gray-900!'),
+ );
+ });
+ });
+
+ describe('highlights', () => {
+ describe('subject line', () => {
+ it('does not highlight less than 50 characters', async () => {
+ const text = 'text less than 50 chars';
+
+ createComponent();
+ await fillText(text);
+
+ expect(findHighlightsText().text()).toEqual(text);
+ expect(findHighlightsMark().text()).toBeFalsy();
+ });
+
+ it('highlights characters over 50 length', async () => {
+ const text =
+ 'text less than 50 chars that should not highlighted. text more than 50 should be highlighted';
+
+ createComponent();
+ await fillText(text);
+
+ expect(findHighlightsText().text()).toEqual(text.slice(0, 50));
+ expect(findHighlightsMark().text()).toEqual(text.slice(50));
+ });
+ });
+
+ describe('body text', () => {
+ it('does not highlight body text less tan 72 characters', async () => {
+ const text = 'subject line\nbody content';
+
+ createComponent();
+ await fillText(text);
+
+ expect(findHighlightsTexts()).toHaveLength(2);
+ expect(findHighlightsMarks().at(1).attributes('style')).toEqual('display: none;');
+ });
+
+ it('highlights body text more than 72 characters', async () => {
+ const text =
+ 'subject line\nbody content that will be highlighted when it is more than 72 characters in length';
+
+ createComponent();
+ await fillText(text);
+
+ expect(findHighlightsTexts()).toHaveLength(2);
+ expect(findHighlightsMarks().at(1).attributes('style')).not.toEqual('display: none;');
+ expect(findHighlightsMarks().at(1).element.textContent).toEqual(' in length');
+ });
+
+ it('highlights body text & subject line', async () => {
+ const text =
+ 'text less than 50 chars that should not highlighted\nbody content that will be highlighted when it is more than 72 characters in length';
+
+ createComponent();
+ await fillText(text);
+
+ expect(findHighlightsTexts()).toHaveLength(2);
+ expect(findHighlightsMarks()).toHaveLength(2);
+ expect(findHighlightsMarks().at(0).element.textContent).toEqual('d');
+ expect(findHighlightsMarks().at(1).element.textContent).toEqual(' in length');
+ });
+ });
+ });
+
+ describe('scrolling textarea', () => {
+ it('updates transform of highlights', async () => {
+ const yCoord = 50;
+
+ createComponent();
+ await fillText('subject line\n\n\n\n\n\n\n\n\n\n\nbody content');
+
+ wrapper.vm.$el.querySelector('textarea').scrollTo(0, yCoord);
+ await nextTick();
+
+ expect(wrapper.vm.scrollTop).toEqual(yCoord);
+ expect(findHighlights().attributes('style')).toEqual('transform: translate3d(0, -50px, 0);');
+ });
+ });
+});
diff --git a/spec/frontend/ide/stores/mutations_spec.js b/spec/frontend/ide/stores/mutations_spec.js
index 23fe23bdef9..4602a0837e0 100644
--- a/spec/frontend/ide/stores/mutations_spec.js
+++ b/spec/frontend/ide/stores/mutations_spec.js
@@ -86,12 +86,12 @@ describe('Multi-file store mutations', () => {
mutations.SET_EMPTY_STATE_SVGS(localState, {
emptyStateSvgPath: 'emptyState',
noChangesStateSvgPath: 'noChanges',
- committedStateSvgPath: 'commited',
+ committedStateSvgPath: 'committed',
});
expect(localState.emptyStateSvgPath).toBe('emptyState');
expect(localState.noChangesStateSvgPath).toBe('noChanges');
- expect(localState.committedStateSvgPath).toBe('commited');
+ expect(localState.committedStateSvgPath).toBe('committed');
});
});
diff --git a/spec/frontend/import_entities/components/group_dropdown_spec.js b/spec/frontend/import_entities/components/group_dropdown_spec.js
index f7aa0e889ea..1c1e1e7ebd4 100644
--- a/spec/frontend/import_entities/components/group_dropdown_spec.js
+++ b/spec/frontend/import_entities/components/group_dropdown_spec.js
@@ -24,14 +24,21 @@ describe('Import entities group dropdown component', () => {
});
it('passes namespaces from props to default slot', () => {
- const namespaces = ['ns1', 'ns2'];
+ const namespaces = [
+ { id: 1, fullPath: 'ns1' },
+ { id: 2, fullPath: 'ns2' },
+ ];
createComponent({ namespaces });
expect(namespacesTracker).toHaveBeenCalledWith({ namespaces });
});
it('filters namespaces based on user input', async () => {
- const namespaces = ['match1', 'some unrelated', 'match2'];
+ const namespaces = [
+ { id: 1, fullPath: 'match1' },
+ { id: 2, fullPath: 'some unrelated' },
+ { id: 3, fullPath: 'match2' },
+ ];
createComponent({ namespaces });
namespacesTracker.mockReset();
@@ -39,6 +46,11 @@ describe('Import entities group dropdown component', () => {
await nextTick();
- expect(namespacesTracker).toHaveBeenCalledWith({ namespaces: ['match1', 'match2'] });
+ expect(namespacesTracker).toHaveBeenCalledWith({
+ namespaces: [
+ { id: 1, fullPath: 'match1' },
+ { id: 3, fullPath: 'match2' },
+ ],
+ });
});
});
diff --git a/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js
index 60f0780fdb3..cd56f573011 100644
--- a/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js
@@ -1,8 +1,6 @@
import { GlButton, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { STATUSES } from '~/import_entities/constants';
import ImportActionsCell from '~/import_entities/import_groups/components/import_actions_cell.vue';
-import { generateFakeEntry } from '../graphql/fixtures';
describe('import actions cell', () => {
let wrapper;
@@ -10,7 +8,9 @@ describe('import actions cell', () => {
const createComponent = (props) => {
wrapper = shallowMount(ImportActionsCell, {
propsData: {
- groupPathRegex: /^[a-zA-Z]+$/,
+ isFinished: false,
+ isAvailableForImport: false,
+ isInvalid: false,
...props,
},
});
@@ -20,10 +20,9 @@ describe('import actions cell', () => {
wrapper.destroy();
});
- describe('when import status is NONE', () => {
+ describe('when group is available for import', () => {
beforeEach(() => {
- const group = generateFakeEntry({ id: 1, status: STATUSES.NONE });
- createComponent({ group });
+ createComponent({ isAvailableForImport: true });
});
it('renders import button', () => {
@@ -37,10 +36,9 @@ describe('import actions cell', () => {
});
});
- describe('when import status is FINISHED', () => {
+ describe('when group is finished', () => {
beforeEach(() => {
- const group = generateFakeEntry({ id: 1, status: STATUSES.FINISHED });
- createComponent({ group });
+ createComponent({ isAvailableForImport: true, isFinished: true });
});
it('renders re-import button', () => {
@@ -58,29 +56,22 @@ describe('import actions cell', () => {
});
});
- it('does not render import button when group import is in progress', () => {
- const group = generateFakeEntry({ id: 1, status: STATUSES.STARTED });
- createComponent({ group });
+ it('does not render import button when group is not available for import', () => {
+ createComponent({ isAvailableForImport: false });
const button = wrapper.findComponent(GlButton);
expect(button.exists()).toBe(false);
});
- it('renders import button as disabled when there are validation errors', () => {
- const group = generateFakeEntry({
- id: 1,
- status: STATUSES.NONE,
- validation_errors: [{ field: 'new_name', message: 'something ' }],
- });
- createComponent({ group });
+ it('renders import button as disabled when group is invalid', () => {
+ createComponent({ isInvalid: true, isAvailableForImport: true });
const button = wrapper.findComponent(GlButton);
expect(button.props().disabled).toBe(true);
});
it('emits import-group event when import button is clicked', () => {
- const group = generateFakeEntry({ id: 1, status: STATUSES.NONE });
- createComponent({ group });
+ createComponent({ isAvailableForImport: true });
const button = wrapper.findComponent(GlButton);
button.vm.$emit('click');
diff --git a/spec/frontend/import_entities/import_groups/components/import_source_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_source_cell_spec.js
index 2a56efd1cbb..f2735d86493 100644
--- a/spec/frontend/import_entities/import_groups/components/import_source_cell_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_source_cell_spec.js
@@ -4,6 +4,11 @@ import { STATUSES } from '~/import_entities/constants';
import ImportSourceCell from '~/import_entities/import_groups/components/import_source_cell.vue';
import { generateFakeEntry } from '../graphql/fixtures';
+const generateFakeTableEntry = ({ flags = {}, ...entry }) => ({
+ ...generateFakeEntry(entry),
+ flags,
+});
+
describe('import source cell', () => {
let wrapper;
let group;
@@ -23,14 +28,14 @@ describe('import source cell', () => {
describe('when group status is NONE', () => {
beforeEach(() => {
- group = generateFakeEntry({ id: 1, status: STATUSES.NONE });
+ group = generateFakeTableEntry({ id: 1, status: STATUSES.NONE });
createComponent({ group });
});
it('renders link to a group', () => {
const link = wrapper.findComponent(GlLink);
- expect(link.attributes().href).toBe(group.web_url);
- expect(link.text()).toContain(group.full_path);
+ expect(link.attributes().href).toBe(group.webUrl);
+ expect(link.text()).toContain(group.fullPath);
});
it('does not render last imported line', () => {
@@ -40,20 +45,24 @@ describe('import source cell', () => {
describe('when group status is FINISHED', () => {
beforeEach(() => {
- group = generateFakeEntry({ id: 1, status: STATUSES.FINISHED });
+ group = generateFakeTableEntry({
+ id: 1,
+ status: STATUSES.FINISHED,
+ flags: {
+ isFinished: true,
+ },
+ });
createComponent({ group });
});
it('renders link to a group', () => {
const link = wrapper.findComponent(GlLink);
- expect(link.attributes().href).toBe(group.web_url);
- expect(link.text()).toContain(group.full_path);
+ expect(link.attributes().href).toBe(group.webUrl);
+ expect(link.text()).toContain(group.fullPath);
});
it('renders last imported line', () => {
- expect(wrapper.text()).toMatchInterpolatedText(
- 'fake_group_1 Last imported to root/last-group1',
- );
+ expect(wrapper.text()).toMatchInterpolatedText('fake_group_1 Last imported to root/group1');
});
});
});
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 f43e545e049..6e3df21e30a 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,39 +1,30 @@
-import {
- GlButton,
- GlEmptyState,
- GlLoadingIcon,
- GlSearchBoxByClick,
- GlDropdown,
- GlDropdownItem,
- GlTable,
-} from '@gitlab/ui';
-import { mount, createLocalVue } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
+import MockAdapter from 'axios-mock-adapter';
import createMockApollo from 'helpers/mock_apollo_helper';
-import stubChildren from 'helpers/stub_children';
-import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
+import httpStatus from '~/lib/utils/http_status';
+import axios from '~/lib/utils/axios_utils';
import { STATUSES } from '~/import_entities/constants';
-import ImportActionsCell from '~/import_entities/import_groups/components/import_actions_cell.vue';
+import { i18n } from '~/import_entities/import_groups/constants';
import ImportTable from '~/import_entities/import_groups/components/import_table.vue';
-import ImportTargetCell from '~/import_entities/import_groups/components/import_target_cell.vue';
import importGroupsMutation from '~/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql';
-import setImportTargetMutation from '~/import_entities/import_groups/graphql/mutations/set_import_target.mutation.graphql';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
import { availableNamespacesFixture, generateFakeEntry } from '../graphql/fixtures';
-const localVue = createLocalVue();
-localVue.use(VueApollo);
+jest.mock('~/flash');
+jest.mock('~/import_entities/import_groups/services/status_poller');
-const GlDropdownStub = stubComponent(GlDropdown, {
- template: '<div><h1 ref="text"><slot name="button-content"></slot></h1><slot></slot></div>',
-});
+Vue.use(VueApollo);
describe('import table', () => {
let wrapper;
let apolloProvider;
+ let axiosMock;
const SOURCE_URL = 'https://demo.host';
const FAKE_GROUP = generateFakeEntry({ id: 1, status: STATUSES.NONE });
@@ -44,76 +35,81 @@ describe('import table', () => {
const FAKE_PAGE_INFO = { page: 1, perPage: 20, total: 40, totalPages: 2 };
const findImportSelectedButton = () =>
- wrapper.findAllComponents(GlButton).wrappers.find((w) => w.text() === 'Import selected');
- const findPaginationDropdown = () => wrapper.findComponent(GlDropdown);
- const findPaginationDropdownText = () => findPaginationDropdown().find({ ref: 'text' }).text();
+ wrapper.findAll('button').wrappers.find((w) => w.text() === 'Import selected');
+ const findImportButtons = () =>
+ wrapper.findAll('button').wrappers.filter((w) => w.text() === 'Import');
+ const findPaginationDropdown = () => wrapper.find('[aria-label="Page size"]');
+ const findPaginationDropdownText = () => findPaginationDropdown().find('button').text();
- // TODO: remove this ugly approach when
- // issue: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1531
- const findTable = () => wrapper.vm.getTableRef();
+ const selectRow = (idx) =>
+ wrapper.findAll('tbody td input[type=checkbox]').at(idx).trigger('click');
- const createComponent = ({ bulkImportSourceGroups }) => {
+ const createComponent = ({ bulkImportSourceGroups, importGroups }) => {
apolloProvider = createMockApollo([], {
Query: {
availableNamespaces: () => availableNamespacesFixture,
bulkImportSourceGroups,
},
Mutation: {
- setTargetNamespace: jest.fn(),
- setNewName: jest.fn(),
- importGroup: jest.fn(),
+ importGroups,
},
});
wrapper = mount(ImportTable, {
propsData: {
groupPathRegex: /.*/,
+ jobsPath: '/fake_job_path',
sourceUrl: SOURCE_URL,
- groupUrlErrorMessage: 'Please choose a group URL with no special characters or spaces.',
- },
- stubs: {
- ...stubChildren(ImportTable),
- GlSprintf: false,
- GlDropdown: GlDropdownStub,
- GlTable: false,
},
- localVue,
apolloProvider,
});
};
+ beforeAll(() => {
+ gon.api_version = 'v4';
+ });
+
+ beforeEach(() => {
+ axiosMock = new MockAdapter(axios);
+ axiosMock.onGet(/.*\/exists$/, () => []).reply(200);
+ });
+
afterEach(() => {
wrapper.destroy();
});
- it('renders loading icon while performing request', async () => {
- createComponent({
- bulkImportSourceGroups: () => new Promise(() => {}),
+ describe('loading state', () => {
+ it('renders loading icon while performing request', async () => {
+ createComponent({
+ bulkImportSourceGroups: () => new Promise(() => {}),
+ });
+ await waitForPromises();
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
- await waitForPromises();
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
- });
+ it('does not renders loading icon when request is completed', async () => {
+ createComponent({
+ bulkImportSourceGroups: () => [],
+ });
+ await waitForPromises();
- it('does not renders loading icon when request is completed', async () => {
- createComponent({
- bulkImportSourceGroups: () => [],
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
});
- await waitForPromises();
-
- 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();
+ describe('empty state', () => {
+ 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('You have no groups to import');
+ expect(wrapper.find(GlEmptyState).props().title).toBe('You have no groups to import');
+ });
});
it('renders import row for each group in response', async () => {
@@ -140,40 +136,51 @@ describe('import table', () => {
expect(wrapper.text()).not.toContain('Showing 1-0');
});
- describe('converts row events to mutation invocations', () => {
- beforeEach(() => {
- createComponent({
- bulkImportSourceGroups: () => ({ nodes: [FAKE_GROUP], pageInfo: FAKE_PAGE_INFO }),
- });
- return waitForPromises();
+ it('invokes importGroups mutation when row button is clicked', async () => {
+ createComponent({
+ bulkImportSourceGroups: () => ({ nodes: [FAKE_GROUP], pageInfo: FAKE_PAGE_INFO }),
});
- it.each`
- event | payload | mutation | variables
- ${'update-target-namespace'} | ${'new-namespace'} | ${setImportTargetMutation} | ${{ sourceGroupId: FAKE_GROUP.id, targetNamespace: 'new-namespace', newName: 'group1' }}
- ${'update-new-name'} | ${'new-name'} | ${setImportTargetMutation} | ${{ sourceGroupId: FAKE_GROUP.id, targetNamespace: 'root', newName: 'new-name' }}
- `('correctly maps $event to mutation', async ({ event, payload, mutation, variables }) => {
- jest.spyOn(apolloProvider.defaultClient, 'mutate');
- wrapper.find(ImportTargetCell).vm.$emit(event, payload);
- await waitForPromises();
- expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({
- mutation,
- variables,
- });
- });
+ jest.spyOn(apolloProvider.defaultClient, 'mutate');
- it('invokes importGroups mutation when row button is clicked', async () => {
- jest.spyOn(apolloProvider.defaultClient, 'mutate');
+ await waitForPromises();
- wrapper.findComponent(ImportActionsCell).vm.$emit('import-group');
- await waitForPromises();
- expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({
- mutation: importGroupsMutation,
- variables: { sourceGroupIds: [FAKE_GROUP.id] },
- });
+ await findImportButtons()[0].trigger('click');
+ expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({
+ mutation: importGroupsMutation,
+ variables: {
+ importRequests: [
+ {
+ newName: FAKE_GROUP.lastImportTarget.newName,
+ sourceGroupId: FAKE_GROUP.id,
+ targetNamespace: availableNamespacesFixture[0].fullPath,
+ },
+ ],
+ },
});
});
+ it('displays error if importing group fails', async () => {
+ createComponent({
+ bulkImportSourceGroups: () => ({ nodes: [FAKE_GROUP], pageInfo: FAKE_PAGE_INFO }),
+ importGroups: () => {
+ throw new Error();
+ },
+ });
+
+ axiosMock.onPost('/import/bulk_imports.json').reply(httpStatus.BAD_REQUEST);
+
+ await waitForPromises();
+ await findImportButtons()[0].trigger('click');
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: i18n.ERROR_IMPORT,
+ }),
+ );
+ });
+
describe('pagination', () => {
const bulkImportSourceGroupsQueryMock = jest
.fn()
@@ -195,10 +202,10 @@ describe('import table', () => {
});
it('updates page size when selected in Dropdown', async () => {
- const otherOption = wrapper.findAllComponents(GlDropdownItem).at(1);
+ const otherOption = findPaginationDropdown().findAll('li p').at(1);
expect(otherOption.text()).toMatchInterpolatedText('50 items per page');
- otherOption.vm.$emit('click');
+ await otherOption.trigger('click');
await waitForPromises();
expect(findPaginationDropdownText()).toMatchInterpolatedText('50 items per page');
@@ -247,7 +254,11 @@ describe('import table', () => {
return waitForPromises();
});
- const findFilterInput = () => wrapper.find(GlSearchBoxByClick);
+ const setFilter = (value) => {
+ const input = wrapper.find('input[placeholder="Filter by source group"]');
+ input.setValue(value);
+ return input.trigger('keydown.enter');
+ };
it('properly passes filter to graphql query when search box is submitted', async () => {
createComponent({
@@ -256,7 +267,7 @@ describe('import table', () => {
await waitForPromises();
const FILTER_VALUE = 'foo';
- findFilterInput().vm.$emit('submit', FILTER_VALUE);
+ await setFilter(FILTER_VALUE);
await waitForPromises();
expect(bulkImportSourceGroupsQueryMock).toHaveBeenCalledWith(
@@ -274,7 +285,7 @@ describe('import table', () => {
await waitForPromises();
const FILTER_VALUE = 'foo';
- findFilterInput().vm.$emit('submit', FILTER_VALUE);
+ await setFilter(FILTER_VALUE);
await waitForPromises();
expect(wrapper.text()).toContain('Showing 1-1 of 40 groups matching filter "foo" from');
@@ -282,12 +293,14 @@ describe('import table', () => {
it('properly resets filter in graphql query when search box is cleared', async () => {
const FILTER_VALUE = 'foo';
- findFilterInput().vm.$emit('submit', FILTER_VALUE);
+ await setFilter(FILTER_VALUE);
await waitForPromises();
bulkImportSourceGroupsQueryMock.mockClear();
await apolloProvider.defaultClient.resetStore();
- findFilterInput().vm.$emit('clear');
+
+ await setFilter('');
+
await waitForPromises();
expect(bulkImportSourceGroupsQueryMock).toHaveBeenCalledWith(
@@ -320,8 +333,8 @@ describe('import table', () => {
}),
});
await waitForPromises();
- wrapper.find(GlTable).vm.$emit('row-selected', [FAKE_GROUPS[0]]);
- await nextTick();
+
+ await selectRow(0);
expect(findImportSelectedButton().props().disabled).toBe(false);
});
@@ -337,7 +350,7 @@ describe('import table', () => {
});
await waitForPromises();
- findTable().selectRow(0);
+ await selectRow(0);
await nextTick();
expect(findImportSelectedButton().props().disabled).toBe(true);
@@ -348,7 +361,6 @@ describe('import table', () => {
generateFakeEntry({
id: 2,
status: STATUSES.NONE,
- validation_errors: [{ field: 'new_name', message: 'FAKE_VALIDATION_ERROR' }],
}),
];
@@ -360,9 +372,9 @@ describe('import table', () => {
});
await waitForPromises();
- // TODO: remove this ugly approach when
- // issue: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1531
- findTable().selectRow(0);
+ await wrapper.find('tbody input[aria-label="New name"]').setValue('');
+ jest.runOnlyPendingTimers();
+ await selectRow(0);
await nextTick();
expect(findImportSelectedButton().props().disabled).toBe(true);
@@ -384,15 +396,28 @@ describe('import table', () => {
jest.spyOn(apolloProvider.defaultClient, 'mutate');
await waitForPromises();
- findTable().selectRow(0);
- findTable().selectRow(1);
+ await selectRow(0);
+ await selectRow(1);
await nextTick();
- findImportSelectedButton().vm.$emit('click');
+ await findImportSelectedButton().trigger('click');
expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({
mutation: importGroupsMutation,
- variables: { sourceGroupIds: [NEW_GROUPS[0].id, NEW_GROUPS[1].id] },
+ variables: {
+ importRequests: [
+ {
+ targetNamespace: availableNamespacesFixture[0].fullPath,
+ newName: NEW_GROUPS[0].lastImportTarget.newName,
+ sourceGroupId: NEW_GROUPS[0].id,
+ },
+ {
+ targetNamespace: availableNamespacesFixture[0].fullPath,
+ newName: NEW_GROUPS[1].lastImportTarget.newName,
+ sourceGroupId: NEW_GROUPS[1].id,
+ },
+ ],
+ },
});
});
});
diff --git a/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js
index be83a61841f..3c2367e22f5 100644
--- a/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js
@@ -3,20 +3,20 @@ import { shallowMount } from '@vue/test-utils';
import ImportGroupDropdown from '~/import_entities/components/group_dropdown.vue';
import { STATUSES } from '~/import_entities/constants';
import ImportTargetCell from '~/import_entities/import_groups/components/import_target_cell.vue';
-import { availableNamespacesFixture } from '../graphql/fixtures';
-
-const getFakeGroup = (status) => ({
- web_url: 'https://fake.host/',
- full_path: 'fake_group_1',
- full_name: 'fake_name_1',
- import_target: {
- target_namespace: 'root',
- new_name: 'group1',
- },
- id: 1,
- validation_errors: [],
- progress: { status },
-});
+import { generateFakeEntry, availableNamespacesFixture } from '../graphql/fixtures';
+
+const generateFakeTableEntry = ({ flags = {}, ...config }) => {
+ const entry = generateFakeEntry(config);
+
+ return {
+ ...entry,
+ importTarget: {
+ targetNamespace: availableNamespacesFixture[0],
+ newName: entry.lastImportTarget.newName,
+ },
+ flags,
+ };
+};
describe('import target cell', () => {
let wrapper;
@@ -31,7 +31,6 @@ describe('import target cell', () => {
propsData: {
availableNamespaces: availableNamespacesFixture,
groupPathRegex: /.*/,
- groupUrlErrorMessage: 'Please choose a group URL with no special characters or spaces.',
...props,
},
});
@@ -44,11 +43,11 @@ describe('import target cell', () => {
describe('events', () => {
beforeEach(() => {
- group = getFakeGroup(STATUSES.NONE);
+ group = generateFakeTableEntry({ id: 1, status: STATUSES.NONE });
createComponent({ group });
});
- it('invokes $event', () => {
+ it('emits update-new-name when input value is changed', () => {
findNameInput().vm.$emit('input', 'demo');
expect(wrapper.emitted('update-new-name')).toBeDefined();
expect(wrapper.emitted('update-new-name')[0][0]).toBe('demo');
@@ -56,18 +55,23 @@ describe('import target cell', () => {
it('emits update-target-namespace when dropdown option is clicked', () => {
const dropdownItem = findNamespaceDropdown().findAllComponents(GlDropdownItem).at(2);
- const dropdownItemText = dropdownItem.text();
dropdownItem.vm.$emit('click');
expect(wrapper.emitted('update-target-namespace')).toBeDefined();
- expect(wrapper.emitted('update-target-namespace')[0][0]).toBe(dropdownItemText);
+ expect(wrapper.emitted('update-target-namespace')[0][0]).toBe(availableNamespacesFixture[1]);
});
});
describe('when entity status is NONE', () => {
beforeEach(() => {
- group = getFakeGroup(STATUSES.NONE);
+ group = generateFakeTableEntry({
+ id: 1,
+ status: STATUSES.NONE,
+ flags: {
+ isAvailableForImport: true,
+ },
+ });
createComponent({ group });
});
@@ -78,7 +82,7 @@ describe('import target cell', () => {
it('renders only no parent option if available namespaces list is empty', () => {
createComponent({
- group: getFakeGroup(STATUSES.NONE),
+ group: generateFakeTableEntry({ id: 1, status: STATUSES.NONE }),
availableNamespaces: [],
});
@@ -92,7 +96,7 @@ describe('import target cell', () => {
it('renders both no parent option and available namespaces list when available namespaces list is not empty', () => {
createComponent({
- group: getFakeGroup(STATUSES.NONE),
+ group: generateFakeTableEntry({ id: 1, status: STATUSES.NONE }),
availableNamespaces: availableNamespacesFixture,
});
@@ -104,9 +108,12 @@ describe('import target cell', () => {
expect(rest).toHaveLength(availableNamespacesFixture.length);
});
- describe('when entity status is SCHEDULING', () => {
+ describe('when entity is not available for import', () => {
beforeEach(() => {
- group = getFakeGroup(STATUSES.SCHEDULING);
+ group = generateFakeTableEntry({
+ id: 1,
+ flags: { isAvailableForImport: false },
+ });
createComponent({ group });
});
@@ -115,9 +122,9 @@ describe('import target cell', () => {
});
});
- describe('when entity status is FINISHED', () => {
+ describe('when entity is available for import', () => {
beforeEach(() => {
- group = getFakeGroup(STATUSES.FINISHED);
+ group = generateFakeTableEntry({ id: 1, flags: { isAvailableForImport: true } });
createComponent({ group });
});
@@ -125,41 +132,4 @@ describe('import target cell', () => {
expect(findNamespaceDropdown().attributes('disabled')).toBe(undefined);
});
});
-
- describe('validations', () => {
- it('reports invalid group name when name is not matching regex', () => {
- createComponent({
- group: {
- ...getFakeGroup(STATUSES.NONE),
- import_target: {
- target_namespace: 'root',
- new_name: 'very`bad`name',
- },
- },
- groupPathRegex: /^[a-zA-Z]+$/,
- });
-
- expect(wrapper.text()).toContain(
- 'Please choose a group URL with no special characters or spaces.',
- );
- });
-
- it('reports invalid group name if relevant validation error exists', async () => {
- const FAKE_ERROR_MESSAGE = 'fake error';
-
- createComponent({
- group: {
- ...getFakeGroup(STATUSES.NONE),
- validation_errors: [
- {
- field: 'new_name',
- message: FAKE_ERROR_MESSAGE,
- },
- ],
- },
- });
-
- expect(wrapper.text()).toContain(FAKE_ERROR_MESSAGE);
- });
- });
});
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 e1d65095888..f3447494578 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
@@ -2,32 +2,27 @@ 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 createFlash from '~/flash';
import { STATUSES } from '~/import_entities/constants';
import {
clientTypenames,
createResolvers,
} from '~/import_entities/import_groups/graphql/client_factory';
-import addValidationErrorMutation from '~/import_entities/import_groups/graphql/mutations/add_validation_error.mutation.graphql';
+import { LocalStorageCache } from '~/import_entities/import_groups/graphql/services/local_storage_cache';
import importGroupsMutation from '~/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql';
-import removeValidationErrorMutation from '~/import_entities/import_groups/graphql/mutations/remove_validation_error.mutation.graphql';
-import setImportProgressMutation from '~/import_entities/import_groups/graphql/mutations/set_import_progress.mutation.graphql';
-import setImportTargetMutation from '~/import_entities/import_groups/graphql/mutations/set_import_target.mutation.graphql';
import updateImportStatusMutation from '~/import_entities/import_groups/graphql/mutations/update_import_status.mutation.graphql';
import availableNamespacesQuery from '~/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql';
-import bulkImportSourceGroupQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_group.query.graphql';
import bulkImportSourceGroupsQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql';
-import groupAndProjectQuery from '~/import_entities/import_groups/graphql/queries/group_and_project.query.graphql';
-import { StatusPoller } from '~/import_entities/import_groups/graphql/services/status_poller';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
import { statusEndpointFixture, availableNamespacesFixture } from './fixtures';
jest.mock('~/flash');
-jest.mock('~/import_entities/import_groups/graphql/services/status_poller', () => ({
- StatusPoller: jest.fn().mockImplementation(function mock() {
- this.startPolling = jest.fn();
+jest.mock('~/import_entities/import_groups/graphql/services/local_storage_cache', () => ({
+ LocalStorageCache: jest.fn().mockImplementation(function mock() {
+ this.get = jest.fn();
+ this.set = jest.fn();
+ this.updateStatusByJobId = jest.fn();
}),
}));
@@ -38,13 +33,6 @@ const FAKE_ENDPOINTS = {
jobs: '/fake_jobs',
};
-const FAKE_GROUP_AND_PROJECTS_QUERY_HANDLER = jest.fn().mockResolvedValue({
- data: {
- existingGroup: null,
- existingProject: null,
- },
-});
-
describe('Bulk import resolvers', () => {
let axiosMockAdapter;
let client;
@@ -58,14 +46,28 @@ describe('Bulk import resolvers', () => {
resolvers: createResolvers({ endpoints: FAKE_ENDPOINTS, ...extraResolverArgs }),
});
- mockedClient.setRequestHandler(groupAndProjectQuery, FAKE_GROUP_AND_PROJECTS_QUERY_HANDLER);
-
return mockedClient;
};
- beforeEach(() => {
+ let results;
+ beforeEach(async () => {
axiosMockAdapter = new MockAdapter(axios);
client = createClient();
+
+ axiosMockAdapter.onGet(FAKE_ENDPOINTS.status).reply(httpStatus.OK, statusEndpointFixture);
+ axiosMockAdapter.onGet(FAKE_ENDPOINTS.availableNamespaces).reply(
+ httpStatus.OK,
+ availableNamespacesFixture.map((ns) => ({
+ id: ns.id,
+ full_path: ns.fullPath,
+ })),
+ );
+
+ client.watchQuery({ query: bulkImportSourceGroupsQuery }).subscribe(({ data }) => {
+ results = data.bulkImportSourceGroups.nodes;
+ });
+
+ return waitForPromises();
});
afterEach(() => {
@@ -74,104 +76,41 @@ describe('Bulk import resolvers', () => {
describe('queries', () => {
describe('availableNamespaces', () => {
- let results;
-
+ let namespacesResults;
beforeEach(async () => {
- axiosMockAdapter
- .onGet(FAKE_ENDPOINTS.availableNamespaces)
- .reply(httpStatus.OK, availableNamespacesFixture);
-
const response = await client.query({ query: availableNamespacesQuery });
- results = response.data.availableNamespaces;
+ namespacesResults = response.data.availableNamespaces;
});
it('mirrors REST endpoint response fields', () => {
const extractRelevantFields = (obj) => ({ id: obj.id, full_path: obj.full_path });
- expect(results.map(extractRelevantFields)).toStrictEqual(
+ expect(namespacesResults.map(extractRelevantFields)).toStrictEqual(
availableNamespacesFixture.map(extractRelevantFields),
);
});
});
- describe('bulkImportSourceGroup', () => {
- beforeEach(async () => {
- axiosMockAdapter.onGet(FAKE_ENDPOINTS.status).reply(httpStatus.OK, statusEndpointFixture);
- axiosMockAdapter
- .onGet(FAKE_ENDPOINTS.availableNamespaces)
- .reply(httpStatus.OK, availableNamespacesFixture);
-
- return client.query({
- query: bulkImportSourceGroupsQuery,
- });
- });
-
- it('returns group', async () => {
- const { id } = statusEndpointFixture.importable_data[0];
- const {
- data: { bulkImportSourceGroup: group },
- } = await client.query({
- query: bulkImportSourceGroupQuery,
- variables: { id: id.toString() },
- });
-
- expect(group).toMatchObject(statusEndpointFixture.importable_data[0]);
- });
- });
-
describe('bulkImportSourceGroups', () => {
- let results;
-
- beforeEach(async () => {
- axiosMockAdapter.onGet(FAKE_ENDPOINTS.status).reply(httpStatus.OK, statusEndpointFixture);
- axiosMockAdapter
- .onGet(FAKE_ENDPOINTS.availableNamespaces)
- .reply(httpStatus.OK, availableNamespacesFixture);
- });
-
it('respects cached import state when provided by group manager', async () => {
- const FAKE_JOB_ID = '1';
- const FAKE_STATUS = 'DEMO_STATUS';
- const FAKE_IMPORT_TARGET = {
- new_name: 'test-name',
- target_namespace: 'test-namespace',
+ const [localStorageCache] = LocalStorageCache.mock.instances;
+ const CACHED_DATA = {
+ progress: {
+ id: 'DEMO',
+ status: 'cached',
+ },
};
- const TARGET_INDEX = 0;
+ localStorageCache.get.mockReturnValueOnce(CACHED_DATA);
- const clientWithMockedManager = createClient({
- GroupsManager: jest.fn().mockImplementation(() => ({
- getImportStateFromStorageByGroupId(groupId) {
- if (groupId === statusEndpointFixture.importable_data[TARGET_INDEX].id) {
- return {
- jobId: FAKE_JOB_ID,
- importState: {
- status: FAKE_STATUS,
- importTarget: FAKE_IMPORT_TARGET,
- },
- };
- }
-
- return null;
- },
- })),
- });
-
- const clientResponse = await clientWithMockedManager.query({
+ const updatedResults = await client.query({
query: bulkImportSourceGroupsQuery,
+ fetchPolicy: 'no-cache',
});
- const clientResults = clientResponse.data.bulkImportSourceGroups.nodes;
-
- expect(clientResults[TARGET_INDEX].import_target).toStrictEqual(FAKE_IMPORT_TARGET);
- expect(clientResults[TARGET_INDEX].progress.status).toBe(FAKE_STATUS);
- });
-
- it('populates each result instance with empty import_target when there are no available namespaces', async () => {
- axiosMockAdapter.onGet(FAKE_ENDPOINTS.availableNamespaces).reply(httpStatus.OK, []);
-
- const response = await client.query({ query: bulkImportSourceGroupsQuery });
- results = response.data.bulkImportSourceGroups.nodes;
- expect(results.every((r) => r.import_target.target_namespace === '')).toBe(true);
+ expect(updatedResults.data.bulkImportSourceGroups.nodes[0].progress).toStrictEqual({
+ __typename: clientTypenames.BulkImportProgress,
+ ...CACHED_DATA.progress,
+ });
});
describe('when called', () => {
@@ -181,37 +120,23 @@ describe('Bulk import resolvers', () => {
});
it('mirrors REST endpoint response fields', () => {
- const MIRRORED_FIELDS = ['id', 'full_name', 'full_path', 'web_url'];
+ const MIRRORED_FIELDS = [
+ { from: 'id', to: 'id' },
+ { from: 'full_name', to: 'fullName' },
+ { from: 'full_path', to: 'fullPath' },
+ { from: 'web_url', to: 'webUrl' },
+ ];
expect(
results.every((r, idx) =>
MIRRORED_FIELDS.every(
- (field) => r[field] === statusEndpointFixture.importable_data[idx][field],
+ (field) => r[field.to] === statusEndpointFixture.importable_data[idx][field.from],
),
),
).toBe(true);
});
- it('populates each result instance with status default to none', () => {
- expect(results.every((r) => r.progress.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('starts polling when request completes', async () => {
- const [statusPoller] = StatusPoller.mock.instances;
- expect(statusPoller.startPolling).toHaveBeenCalled();
- });
-
- it('requests validation status when request completes', async () => {
- expect(FAKE_GROUP_AND_PROJECTS_QUERY_HANDLER).not.toHaveBeenCalled();
- jest.runOnlyPendingTimers();
- expect(FAKE_GROUP_AND_PROJECTS_QUERY_HANDLER).toHaveBeenCalled();
+ it('populates each result instance with empty status', () => {
+ expect(results.every((r) => r.progress === null)).toBe(true);
});
});
@@ -223,6 +148,7 @@ describe('Bulk import resolvers', () => {
`(
'properly passes GraphQL variable $variable as REST $queryParam query parameter',
async ({ variable, queryParam, value }) => {
+ axiosMockAdapter.resetHistory();
await client.query({
query: bulkImportSourceGroupsQuery,
variables: { [variable]: value },
@@ -237,275 +163,61 @@ describe('Bulk import resolvers', () => {
});
describe('mutations', () => {
- const GROUP_ID = 1;
-
beforeEach(() => {
- client.writeQuery({
- query: bulkImportSourceGroupsQuery,
- data: {
- bulkImportSourceGroups: {
- nodes: [
- {
- __typename: clientTypenames.BulkImportSourceGroup,
- id: GROUP_ID,
- progress: {
- id: `test-${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',
- },
- last_import_target: {
- target_namespace: 'root',
- new_name: 'group1',
- },
- validation_errors: [],
- },
- ],
- pageInfo: {
- page: 1,
- perPage: 20,
- total: 37,
- totalPages: 2,
- },
- },
- },
- });
+ axiosMockAdapter.onPost(FAKE_ENDPOINTS.createBulkImport).reply(httpStatus.OK, { id: 1 });
});
- describe('setImportTarget', () => {
- it('updates group target namespace and name', async () => {
- const NEW_TARGET_NAMESPACE = 'target';
- const NEW_NAME = 'new';
-
- const {
- data: {
- setImportTarget: {
- id: idInResponse,
- import_target: { target_namespace: namespaceInResponse, new_name: newNameInResponse },
- },
- },
- } = await client.mutate({
- mutation: setImportTargetMutation,
- variables: {
- sourceGroupId: GROUP_ID,
- targetNamespace: NEW_TARGET_NAMESPACE,
- newName: NEW_NAME,
- },
- });
-
- expect(idInResponse).toBe(GROUP_ID);
- expect(namespaceInResponse).toBe(NEW_TARGET_NAMESPACE);
- expect(newNameInResponse).toBe(NEW_NAME);
- });
-
- it('invokes validation', async () => {
- const NEW_TARGET_NAMESPACE = 'target';
- const NEW_NAME = 'new';
-
+ describe('importGroup', () => {
+ it('sets import status to CREATED when request completes', async () => {
await client.mutate({
- mutation: setImportTargetMutation,
+ mutation: importGroupsMutation,
variables: {
- sourceGroupId: GROUP_ID,
- targetNamespace: NEW_TARGET_NAMESPACE,
- newName: NEW_NAME,
+ importRequests: [
+ {
+ sourceGroupId: statusEndpointFixture.importable_data[0].id,
+ newName: 'test',
+ targetNamespace: 'root',
+ },
+ ],
},
});
- expect(FAKE_GROUP_AND_PROJECTS_QUERY_HANDLER).toHaveBeenCalledWith({
- fullPath: `${NEW_TARGET_NAMESPACE}/${NEW_NAME}`,
- });
- });
- });
-
- describe('importGroup', () => {
- it('sets status to SCHEDULING when request initiates', async () => {
- axiosMockAdapter.onPost(FAKE_ENDPOINTS.createBulkImport).reply(() => new Promise(() => {}));
-
- client.mutate({
- mutation: importGroupsMutation,
- variables: { sourceGroupIds: [GROUP_ID] },
- });
- await waitForPromises();
-
- const {
- bulkImportSourceGroups: { nodes: intermediateResults },
- } = client.readQuery({
- query: bulkImportSourceGroupsQuery,
- });
-
- expect(intermediateResults[0].progress.status).toBe(STATUSES.SCHEDULING);
- });
-
- describe('when request completes', () => {
- let results;
-
- beforeEach(() => {
- client
- .watchQuery({
- query: bulkImportSourceGroupsQuery,
- fetchPolicy: 'cache-only',
- })
- .subscribe(({ data }) => {
- results = data.bulkImportSourceGroups.nodes;
- });
- });
-
- it('sets import status to CREATED when request completes', async () => {
- axiosMockAdapter.onPost(FAKE_ENDPOINTS.createBulkImport).reply(httpStatus.OK, { id: 1 });
- await client.mutate({
- mutation: importGroupsMutation,
- variables: { sourceGroupIds: [GROUP_ID] },
- });
- await waitForPromises();
-
- expect(results[0].progress.status).toBe(STATUSES.CREATED);
- });
-
- it('resets status to NONE if request fails', async () => {
- axiosMockAdapter
- .onPost(FAKE_ENDPOINTS.createBulkImport)
- .reply(httpStatus.INTERNAL_SERVER_ERROR);
-
- client
- .mutate({
- mutation: [importGroupsMutation],
- variables: { sourceGroupIds: [GROUP_ID] },
- })
- .catch(() => {});
- await waitForPromises();
-
- expect(results[0].progress.status).toBe(STATUSES.NONE);
- });
- });
-
- it('shows default error message when server error is not provided', async () => {
- axiosMockAdapter
- .onPost(FAKE_ENDPOINTS.createBulkImport)
- .reply(httpStatus.INTERNAL_SERVER_ERROR);
-
- client
- .mutate({
- mutation: importGroupsMutation,
- variables: { sourceGroupIds: [GROUP_ID] },
- })
- .catch(() => {});
- await waitForPromises();
-
- expect(createFlash).toHaveBeenCalledWith({ message: 'Importing the group failed' });
- });
-
- it('shows provided error message when error is included in backend response', async () => {
- const CUSTOM_MESSAGE = 'custom message';
-
- axiosMockAdapter
- .onPost(FAKE_ENDPOINTS.createBulkImport)
- .reply(httpStatus.INTERNAL_SERVER_ERROR, { error: CUSTOM_MESSAGE });
-
- client
- .mutate({
- mutation: importGroupsMutation,
- variables: { sourceGroupIds: [GROUP_ID] },
- })
- .catch(() => {});
- await waitForPromises();
-
- expect(createFlash).toHaveBeenCalledWith({ message: CUSTOM_MESSAGE });
+ await axios.waitForAll();
+ expect(results[0].progress.status).toBe(STATUSES.CREATED);
});
});
- it('setImportProgress updates group progress and sets import target', async () => {
+ it('updateImportStatus updates status', async () => {
const NEW_STATUS = 'dummy';
- const FAKE_JOB_ID = 5;
- const IMPORT_TARGET = {
- __typename: 'ClientBulkImportTarget',
- new_name: 'fake_name',
- target_namespace: 'fake_target',
- };
- const {
- data: {
- setImportProgress: { progress, last_import_target: lastImportTarget },
- },
- } = await client.mutate({
- mutation: setImportProgressMutation,
+ await client.mutate({
+ mutation: importGroupsMutation,
variables: {
- sourceGroupId: GROUP_ID,
- status: NEW_STATUS,
- jobId: FAKE_JOB_ID,
- importTarget: IMPORT_TARGET,
+ importRequests: [
+ {
+ sourceGroupId: statusEndpointFixture.importable_data[0].id,
+ newName: 'test',
+ targetNamespace: 'root',
+ },
+ ],
},
});
+ await axios.waitForAll();
+ await waitForPromises();
- expect(lastImportTarget).toStrictEqual(IMPORT_TARGET);
-
- expect(progress).toStrictEqual({
- __typename: clientTypenames.BulkImportProgress,
- id: FAKE_JOB_ID,
- status: NEW_STATUS,
- });
- });
+ const { id } = results[0].progress;
- it('updateImportStatus returns new status', async () => {
- const NEW_STATUS = 'dummy';
- const FAKE_JOB_ID = 5;
const {
data: { updateImportStatus: statusInResponse },
} = await client.mutate({
mutation: updateImportStatusMutation,
- variables: { id: FAKE_JOB_ID, status: NEW_STATUS },
+ variables: { id, status: NEW_STATUS },
});
expect(statusInResponse).toStrictEqual({
__typename: clientTypenames.BulkImportProgress,
- id: FAKE_JOB_ID,
+ id,
status: NEW_STATUS,
});
});
-
- it('addValidationError adds error to group', async () => {
- const FAKE_FIELD = 'some-field';
- const FAKE_MESSAGE = 'some-message';
- const {
- data: {
- addValidationError: { validation_errors: validationErrors },
- },
- } = await client.mutate({
- mutation: addValidationErrorMutation,
- variables: { sourceGroupId: GROUP_ID, field: FAKE_FIELD, message: FAKE_MESSAGE },
- });
-
- expect(validationErrors).toStrictEqual([
- {
- __typename: clientTypenames.BulkImportValidationError,
- field: FAKE_FIELD,
- message: FAKE_MESSAGE,
- },
- ]);
- });
-
- it('removeValidationError removes error from group', async () => {
- const FAKE_FIELD = 'some-field';
- const FAKE_MESSAGE = 'some-message';
-
- await client.mutate({
- mutation: addValidationErrorMutation,
- variables: { sourceGroupId: GROUP_ID, field: FAKE_FIELD, message: FAKE_MESSAGE },
- });
-
- const {
- data: {
- removeValidationError: { validation_errors: validationErrors },
- },
- } = await client.mutate({
- mutation: removeValidationErrorMutation,
- variables: { sourceGroupId: GROUP_ID, field: FAKE_FIELD },
- });
-
- expect(validationErrors).toStrictEqual([]);
- });
});
});
diff --git a/spec/frontend/import_entities/import_groups/graphql/fixtures.js b/spec/frontend/import_entities/import_groups/graphql/fixtures.js
index d1bd52693b6..5f6f9987a8f 100644
--- a/spec/frontend/import_entities/import_groups/graphql/fixtures.js
+++ b/spec/frontend/import_entities/import_groups/graphql/fixtures.js
@@ -1,24 +1,24 @@
+import { STATUSES } from '~/import_entities/constants';
import { clientTypenames } from '~/import_entities/import_groups/graphql/client_factory';
export const generateFakeEntry = ({ id, status, ...rest }) => ({
__typename: clientTypenames.BulkImportSourceGroup,
- web_url: `https://fake.host/${id}`,
- full_path: `fake_group_${id}`,
- full_name: `fake_name_${id}`,
- import_target: {
- target_namespace: 'root',
- new_name: `group${id}`,
- },
- last_import_target: {
- target_namespace: 'root',
- new_name: `last-group${id}`,
+ webUrl: `https://fake.host/${id}`,
+ fullPath: `fake_group_${id}`,
+ fullName: `fake_name_${id}`,
+ lastImportTarget: {
+ id,
+ targetNamespace: 'root',
+ newName: `group${id}`,
},
id,
- progress: {
- id: `test-${id}`,
- status,
- },
- validation_errors: [],
+ progress:
+ status === STATUSES.NONE || status === STATUSES.PENDING
+ ? null
+ : {
+ id,
+ status,
+ },
...rest,
});
@@ -51,9 +51,9 @@ export const statusEndpointFixture = {
],
};
-export const availableNamespacesFixture = [
- { id: 24, full_path: 'Commit451' },
- { id: 22, full_path: 'gitlab-org' },
- { id: 23, full_path: 'gnuwget' },
- { id: 25, full_path: 'jashkenas' },
-];
+export const availableNamespacesFixture = Object.freeze([
+ { id: 24, fullPath: 'Commit451' },
+ { id: 22, fullPath: 'gitlab-org' },
+ { id: 23, fullPath: 'gnuwget' },
+ { id: 25, fullPath: 'jashkenas' },
+]);
diff --git a/spec/frontend/import_entities/import_groups/graphql/services/local_storage_cache_spec.js b/spec/frontend/import_entities/import_groups/graphql/services/local_storage_cache_spec.js
new file mode 100644
index 00000000000..b44a2767ad8
--- /dev/null
+++ b/spec/frontend/import_entities/import_groups/graphql/services/local_storage_cache_spec.js
@@ -0,0 +1,61 @@
+import {
+ KEY,
+ LocalStorageCache,
+} from '~/import_entities/import_groups/graphql/services/local_storage_cache';
+
+describe('Local storage cache', () => {
+ let cache;
+ let storage;
+
+ beforeEach(() => {
+ storage = {
+ getItem: jest.fn(),
+ setItem: jest.fn(),
+ };
+
+ cache = new LocalStorageCache({ storage });
+ });
+
+ describe('storage management', () => {
+ const IMPORT_URL = 'http://fake.url';
+
+ it('loads state from storage on creation', () => {
+ expect(storage.getItem).toHaveBeenCalledWith(KEY);
+ });
+
+ it('saves to storage when set is called', () => {
+ const STORAGE_CONTENT = { fake: 'content ' };
+ cache.set(IMPORT_URL, STORAGE_CONTENT);
+ expect(storage.setItem).toHaveBeenCalledWith(
+ KEY,
+ JSON.stringify({ [IMPORT_URL]: STORAGE_CONTENT }),
+ );
+ });
+
+ it('updates status by job id', () => {
+ const CHANGED_STATUS = 'changed';
+ const JOB_ID = 2;
+
+ cache.set(IMPORT_URL, {
+ progress: {
+ id: JOB_ID,
+ status: 'original',
+ },
+ });
+
+ cache.updateStatusByJobId(JOB_ID, CHANGED_STATUS);
+
+ expect(storage.setItem).toHaveBeenCalledWith(
+ KEY,
+ JSON.stringify({
+ [IMPORT_URL]: {
+ progress: {
+ id: JOB_ID,
+ status: CHANGED_STATUS,
+ },
+ },
+ }),
+ );
+ });
+ });
+});
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
deleted file mode 100644
index f06babcb149..00000000000
--- a/spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import {
- KEY,
- SourceGroupsManager,
-} from '~/import_entities/import_groups/graphql/services/source_groups_manager';
-
-const FAKE_SOURCE_URL = 'http://demo.host';
-
-describe('SourceGroupsManager', () => {
- let manager;
- let storage;
-
- beforeEach(() => {
- storage = {
- getItem: jest.fn(),
- setItem: jest.fn(),
- };
-
- manager = new SourceGroupsManager({ storage, sourceUrl: FAKE_SOURCE_URL });
- });
-
- describe('storage management', () => {
- const IMPORT_ID = 1;
- const IMPORT_TARGET = { new_name: 'demo', target_namespace: 'foo' };
- const STATUS = 'FAKE_STATUS';
- const FAKE_GROUP = { id: 1, import_target: IMPORT_TARGET, status: STATUS };
-
- it('loads state from storage on creation', () => {
- expect(storage.getItem).toHaveBeenCalledWith(KEY);
- });
-
- it('saves to storage when createImportState is called', () => {
- const FAKE_STATUS = 'fake;';
- manager.createImportState(IMPORT_ID, { status: FAKE_STATUS, groups: [FAKE_GROUP] });
- const storedObject = JSON.parse(storage.setItem.mock.calls[0][1]);
- expect(Object.values(storedObject)[0]).toStrictEqual({
- status: FAKE_STATUS,
- groups: [
- {
- id: FAKE_GROUP.id,
- importTarget: IMPORT_TARGET,
- },
- ],
- });
- });
-
- it('updates storage when previous state is available', () => {
- const CHANGED_STATUS = 'changed';
-
- manager.createImportState(IMPORT_ID, { status: STATUS, groups: [FAKE_GROUP] });
-
- manager.updateImportProgress(IMPORT_ID, CHANGED_STATUS);
- const storedObject = JSON.parse(storage.setItem.mock.calls[1][1]);
- expect(Object.values(storedObject)[0]).toStrictEqual({
- status: CHANGED_STATUS,
- groups: [
- {
- id: FAKE_GROUP.id,
- importTarget: IMPORT_TARGET,
- },
- ],
- });
- });
- });
-});
diff --git a/spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js b/spec/frontend/import_entities/import_groups/services/status_poller_spec.js
index 9c47647c430..01f976562c6 100644
--- a/spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js
+++ b/spec/frontend/import_entities/import_groups/services/status_poller_spec.js
@@ -2,19 +2,13 @@ import MockAdapter from 'axios-mock-adapter';
import Visibility from 'visibilityjs';
import createFlash from '~/flash';
import { STATUSES } from '~/import_entities/constants';
-import { StatusPoller } from '~/import_entities/import_groups/graphql/services/status_poller';
+import { StatusPoller } from '~/import_entities/import_groups/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 FAKE_POLL_PATH = '/fake/poll/path';
@@ -81,6 +75,7 @@ describe('Bulk import status poller', () => {
const [pollInstance] = Poll.mock.instances;
poller.startPolling();
+ await Promise.resolve();
expect(pollInstance.makeRequest).toHaveBeenCalled();
});
diff --git a/spec/frontend/incidents/components/incidents_list_spec.js b/spec/frontend/incidents/components/incidents_list_spec.js
index 8d4ccab2a40..48545ffd2d6 100644
--- a/spec/frontend/incidents/components/incidents_list_spec.js
+++ b/spec/frontend/incidents/components/incidents_list_spec.js
@@ -78,6 +78,7 @@ describe('Incidents List', () => {
authorUsernameQuery: '',
assigneeUsernameQuery: '',
slaFeatureAvailable: true,
+ canCreateIncident: true,
...provide,
},
stubs: {
@@ -105,21 +106,23 @@ describe('Incidents List', () => {
describe('empty state', () => {
const {
- emptyState: { title, emptyClosedTabTitle, description },
+ emptyState: { title, emptyClosedTabTitle, description, cannotCreateIncidentDescription },
} = I18N;
it.each`
- statusFilter | all | closed | expectedTitle | expectedDescription
- ${'all'} | ${2} | ${1} | ${title} | ${description}
- ${'open'} | ${2} | ${0} | ${title} | ${description}
- ${'closed'} | ${0} | ${0} | ${title} | ${description}
- ${'closed'} | ${2} | ${0} | ${emptyClosedTabTitle} | ${undefined}
+ statusFilter | all | closed | expectedTitle | canCreateIncident | expectedDescription
+ ${'all'} | ${2} | ${1} | ${title} | ${true} | ${description}
+ ${'open'} | ${2} | ${0} | ${title} | ${true} | ${description}
+ ${'closed'} | ${0} | ${0} | ${title} | ${true} | ${description}
+ ${'closed'} | ${2} | ${0} | ${emptyClosedTabTitle} | ${true} | ${undefined}
+ ${'all'} | ${2} | ${1} | ${title} | ${false} | ${cannotCreateIncidentDescription}
`(
`when active tab is $statusFilter and there are $all incidents in total and $closed closed incidents, the empty state
has title: $expectedTitle and description: $expectedDescription`,
- ({ statusFilter, all, closed, expectedTitle, expectedDescription }) => {
+ ({ statusFilter, all, closed, expectedTitle, expectedDescription, canCreateIncident }) => {
mountComponent({
data: { incidents: { list: [] }, incidentsCount: { all, closed }, statusFilter },
+ provide: { canCreateIncident },
loading: false,
});
expect(findEmptyState().exists()).toBe(true);
@@ -219,6 +222,15 @@ describe('Incidents List', () => {
expect(findCreateIncidentBtn().exists()).toBe(false);
});
+ it("doesn't show the button when user does not have incident creation permissions", () => {
+ mountComponent({
+ data: { incidents: { list: mockIncidents }, incidentsCount: {} },
+ provide: { canCreateIncident: false },
+ loading: false,
+ });
+ expect(findCreateIncidentBtn().exists()).toBe(false);
+ });
+
it('should track create new incident button', async () => {
findCreateIncidentBtn().vm.$emit('click');
await wrapper.vm.$nextTick();
diff --git a/spec/frontend/integrations/edit/components/dynamic_field_spec.js b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
index da8a2f41c1b..bf044e388ea 100644
--- a/spec/frontend/integrations/edit/components/dynamic_field_spec.js
+++ b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
@@ -35,136 +35,145 @@ describe('DynamicField', () => {
const findGlFormTextarea = () => wrapper.findComponent(GlFormTextarea);
describe('template', () => {
- describe.each([
- [true, 'disabled', 'readonly'],
- [false, undefined, undefined],
- ])('dynamic field, when isInheriting = `%p`', (isInheriting, disabled, readonly) => {
- describe('type is checkbox', () => {
- beforeEach(() => {
- createComponent(
- {
- type: 'checkbox',
- },
- isInheriting,
- );
- });
+ describe.each`
+ isInheriting | disabled | readonly | checkboxLabel
+ ${true} | ${'disabled'} | ${'readonly'} | ${undefined}
+ ${false} | ${undefined} | ${undefined} | ${'Custom checkbox label'}
+ `(
+ 'dynamic field, when isInheriting = `%p`',
+ ({ isInheriting, disabled, readonly, checkboxLabel }) => {
+ describe('type is checkbox', () => {
+ beforeEach(() => {
+ createComponent(
+ {
+ type: 'checkbox',
+ checkboxLabel,
+ },
+ isInheriting,
+ );
+ });
- it(`renders GlFormCheckbox, which ${isInheriting ? 'is' : 'is not'} disabled`, () => {
- expect(findGlFormCheckbox().exists()).toBe(true);
- expect(findGlFormCheckbox().find('[type=checkbox]').attributes('disabled')).toBe(
- disabled,
- );
- });
+ it(`renders GlFormCheckbox, which ${isInheriting ? 'is' : 'is not'} disabled`, () => {
+ expect(findGlFormCheckbox().exists()).toBe(true);
+ expect(findGlFormCheckbox().find('[type=checkbox]').attributes('disabled')).toBe(
+ disabled,
+ );
+ });
- it('does not render other types of input', () => {
- expect(findGlFormSelect().exists()).toBe(false);
- expect(findGlFormTextarea().exists()).toBe(false);
- expect(findGlFormInput().exists()).toBe(false);
- });
- });
+ it(`renders GlFormCheckbox with correct text content when checkboxLabel is ${checkboxLabel}`, () => {
+ expect(findGlFormCheckbox().text()).toBe(checkboxLabel ?? defaultProps.title);
+ });
- describe('type is select', () => {
- beforeEach(() => {
- createComponent(
- {
- type: 'select',
- choices: [
- ['all', 'All details'],
- ['standard', 'Standard'],
- ],
- },
- isInheriting,
- );
+ it('does not render other types of input', () => {
+ expect(findGlFormSelect().exists()).toBe(false);
+ expect(findGlFormTextarea().exists()).toBe(false);
+ expect(findGlFormInput().exists()).toBe(false);
+ });
});
- it(`renders GlFormSelect, which ${isInheriting ? 'is' : 'is not'} disabled`, () => {
- expect(findGlFormSelect().exists()).toBe(true);
- expect(findGlFormSelect().findAll('option')).toHaveLength(2);
- expect(findGlFormSelect().find('select').attributes('disabled')).toBe(disabled);
- });
+ describe('type is select', () => {
+ beforeEach(() => {
+ createComponent(
+ {
+ type: 'select',
+ choices: [
+ ['all', 'All details'],
+ ['standard', 'Standard'],
+ ],
+ },
+ isInheriting,
+ );
+ });
- it('does not render other types of input', () => {
- expect(findGlFormCheckbox().exists()).toBe(false);
- expect(findGlFormTextarea().exists()).toBe(false);
- expect(findGlFormInput().exists()).toBe(false);
- });
- });
+ it(`renders GlFormSelect, which ${isInheriting ? 'is' : 'is not'} disabled`, () => {
+ expect(findGlFormSelect().exists()).toBe(true);
+ expect(findGlFormSelect().findAll('option')).toHaveLength(2);
+ expect(findGlFormSelect().find('select').attributes('disabled')).toBe(disabled);
+ });
- describe('type is textarea', () => {
- beforeEach(() => {
- createComponent(
- {
- type: 'textarea',
- },
- isInheriting,
- );
+ it('does not render other types of input', () => {
+ expect(findGlFormCheckbox().exists()).toBe(false);
+ expect(findGlFormTextarea().exists()).toBe(false);
+ expect(findGlFormInput().exists()).toBe(false);
+ });
});
- it(`renders GlFormTextarea, which ${isInheriting ? 'is' : 'is not'} readonly`, () => {
- expect(findGlFormTextarea().exists()).toBe(true);
- expect(findGlFormTextarea().find('textarea').attributes('readonly')).toBe(readonly);
- });
+ describe('type is textarea', () => {
+ beforeEach(() => {
+ createComponent(
+ {
+ type: 'textarea',
+ },
+ isInheriting,
+ );
+ });
- it('does not render other types of input', () => {
- expect(findGlFormCheckbox().exists()).toBe(false);
- expect(findGlFormSelect().exists()).toBe(false);
- expect(findGlFormInput().exists()).toBe(false);
- });
- });
+ it(`renders GlFormTextarea, which ${isInheriting ? 'is' : 'is not'} readonly`, () => {
+ expect(findGlFormTextarea().exists()).toBe(true);
+ expect(findGlFormTextarea().find('textarea').attributes('readonly')).toBe(readonly);
+ });
- describe('type is password', () => {
- beforeEach(() => {
- createComponent(
- {
- type: 'password',
- },
- isInheriting,
- );
+ it('does not render other types of input', () => {
+ expect(findGlFormCheckbox().exists()).toBe(false);
+ expect(findGlFormSelect().exists()).toBe(false);
+ expect(findGlFormInput().exists()).toBe(false);
+ });
});
- it(`renders GlFormInput, which ${isInheriting ? 'is' : 'is not'} readonly`, () => {
- expect(findGlFormInput().exists()).toBe(true);
- expect(findGlFormInput().attributes('type')).toBe('password');
- expect(findGlFormInput().attributes('readonly')).toBe(readonly);
- });
+ describe('type is password', () => {
+ beforeEach(() => {
+ createComponent(
+ {
+ type: 'password',
+ },
+ isInheriting,
+ );
+ });
- it('does not render other types of input', () => {
- expect(findGlFormCheckbox().exists()).toBe(false);
- expect(findGlFormSelect().exists()).toBe(false);
- expect(findGlFormTextarea().exists()).toBe(false);
- });
- });
+ it(`renders GlFormInput, which ${isInheriting ? 'is' : 'is not'} readonly`, () => {
+ expect(findGlFormInput().exists()).toBe(true);
+ expect(findGlFormInput().attributes('type')).toBe('password');
+ expect(findGlFormInput().attributes('readonly')).toBe(readonly);
+ });
- describe('type is text', () => {
- beforeEach(() => {
- createComponent(
- {
- type: 'text',
- required: true,
- },
- isInheriting,
- );
+ it('does not render other types of input', () => {
+ expect(findGlFormCheckbox().exists()).toBe(false);
+ expect(findGlFormSelect().exists()).toBe(false);
+ expect(findGlFormTextarea().exists()).toBe(false);
+ });
});
- it(`renders GlFormInput, which ${isInheriting ? 'is' : 'is not'} readonly`, () => {
- expect(findGlFormInput().exists()).toBe(true);
- expect(findGlFormInput().attributes()).toMatchObject({
- type: 'text',
- id: 'service_project_url',
- name: 'service[project_url]',
- placeholder: defaultProps.placeholder,
- required: 'required',
+ describe('type is text', () => {
+ beforeEach(() => {
+ createComponent(
+ {
+ type: 'text',
+ required: true,
+ },
+ isInheriting,
+ );
});
- expect(findGlFormInput().attributes('readonly')).toBe(readonly);
- });
- it('does not render other types of input', () => {
- expect(findGlFormCheckbox().exists()).toBe(false);
- expect(findGlFormSelect().exists()).toBe(false);
- expect(findGlFormTextarea().exists()).toBe(false);
+ it(`renders GlFormInput, which ${isInheriting ? 'is' : 'is not'} readonly`, () => {
+ expect(findGlFormInput().exists()).toBe(true);
+ expect(findGlFormInput().attributes()).toMatchObject({
+ type: 'text',
+ id: 'service_project_url',
+ name: 'service[project_url]',
+ placeholder: defaultProps.placeholder,
+ required: 'required',
+ });
+ expect(findGlFormInput().attributes('readonly')).toBe(readonly);
+ });
+
+ it('does not render other types of input', () => {
+ expect(findGlFormCheckbox().exists()).toBe(false);
+ expect(findGlFormSelect().exists()).toBe(false);
+ expect(findGlFormTextarea().exists()).toBe(false);
+ });
});
- });
- });
+ },
+ );
describe('help text', () => {
it('renders description with help text', () => {
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 119afbfecfe..3a664b652ac 100644
--- a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
+++ b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
@@ -1,7 +1,10 @@
import { GlFormCheckbox, GlFormInput } from '@gitlab/ui';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { GET_JIRA_ISSUE_TYPES_EVENT } from '~/integrations/constants';
+import {
+ GET_JIRA_ISSUE_TYPES_EVENT,
+ VALIDATE_INTEGRATION_FORM_EVENT,
+} from '~/integrations/constants';
import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue';
import eventHub from '~/integrations/edit/event_hub';
import { createStore } from '~/integrations/edit/store';
@@ -17,12 +20,17 @@ describe('JiraIssuesFields', () => {
upgradePlanPath: 'https://gitlab.com',
};
- const createComponent = ({ isInheriting = false, props, ...options } = {}) => {
+ const createComponent = ({
+ isInheriting = false,
+ mountFn = mountExtended,
+ props,
+ ...options
+ } = {}) => {
store = createStore({
defaultState: isInheriting ? {} : undefined,
});
- wrapper = mountExtended(JiraIssuesFields, {
+ wrapper = mountFn(JiraIssuesFields, {
propsData: { ...defaultProps, ...props },
store,
stubs: ['jira-issue-creation-vulnerabilities'],
@@ -38,12 +46,19 @@ describe('JiraIssuesFields', () => {
const findEnableCheckboxDisabled = () =>
findEnableCheckbox().find('[type=checkbox]').attributes('disabled');
const findProjectKey = () => wrapper.findComponent(GlFormInput);
+ const findProjectKeyFormGroup = () => wrapper.findByTestId('project-key-form-group');
const findPremiumUpgradeCTA = () => wrapper.findByTestId('premium-upgrade-cta');
const findUltimateUpgradeCTA = () => wrapper.findByTestId('ultimate-upgrade-cta');
const findJiraForVulnerabilities = () => wrapper.findByTestId('jira-for-vulnerabilities');
+ const findConflictWarning = () => wrapper.findByTestId('conflict-warning-text');
const setEnableCheckbox = async (isEnabled = true) =>
findEnableCheckbox().vm.$emit('input', isEnabled);
+ const assertProjectKeyState = (expectedStateValue) => {
+ expect(findProjectKey().attributes('state')).toBe(expectedStateValue);
+ expect(findProjectKeyFormGroup().attributes('state')).toBe(expectedStateValue);
+ };
+
describe('template', () => {
describe.each`
showJiraIssuesIntegration | showJiraVulnerabilitiesIntegration
@@ -151,19 +166,18 @@ describe('JiraIssuesFields', () => {
});
describe('GitLab issues warning', () => {
- const expectedText = 'Consider disabling GitLab issues';
-
- it('contains warning when GitLab issues is enabled', () => {
- createComponent();
-
- expect(wrapper.text()).toContain(expectedText);
- });
-
- it('does not contain warning when GitLab issues is disabled', () => {
- createComponent({ props: { gitlabIssuesEnabled: false } });
-
- expect(wrapper.text()).not.toContain(expectedText);
- });
+ it.each`
+ gitlabIssuesEnabled | scenario
+ ${true} | ${'displays conflict warning'}
+ ${false} | ${'does not display conflict warning'}
+ `(
+ '$scenario when `gitlabIssuesEnabled` is `$gitlabIssuesEnabled`',
+ ({ gitlabIssuesEnabled }) => {
+ createComponent({ props: { gitlabIssuesEnabled } });
+
+ expect(findConflictWarning().exists()).toBe(gitlabIssuesEnabled);
+ },
+ );
});
describe('Vulnerabilities creation', () => {
@@ -211,5 +225,44 @@ describe('JiraIssuesFields', () => {
expect(eventHubEmitSpy).toHaveBeenCalledWith(GET_JIRA_ISSUE_TYPES_EVENT);
});
});
+
+ describe('Project key input field', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ initialProjectKey: '',
+ initialEnableJiraIssues: true,
+ },
+ mountFn: shallowMountExtended,
+ });
+ });
+
+ it('sets Project Key `state` attribute to `true` by default', () => {
+ assertProjectKeyState('true');
+ });
+
+ describe('when event hub recieves `VALIDATE_INTEGRATION_FORM_EVENT` event', () => {
+ describe('with no project key', () => {
+ it('sets Project Key `state` attribute to `undefined`', async () => {
+ eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
+ await wrapper.vm.$nextTick();
+
+ assertProjectKeyState(undefined);
+ });
+ });
+
+ describe('when project key is set', () => {
+ it('sets Project Key `state` attribute to `true`', async () => {
+ eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
+
+ // set the project key
+ await findProjectKey().vm.$emit('input', 'AB');
+ await wrapper.vm.$nextTick();
+
+ assertProjectKeyState('true');
+ });
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/integrations/integration_settings_form_spec.js b/spec/frontend/integrations/integration_settings_form_spec.js
index f8f3f0fd318..c35d178e518 100644
--- a/spec/frontend/integrations/integration_settings_form_spec.js
+++ b/spec/frontend/integrations/integration_settings_form_spec.js
@@ -1,27 +1,38 @@
import MockAdaptor from 'axios-mock-adapter';
import IntegrationSettingsForm from '~/integrations/integration_settings_form';
+import eventHub from '~/integrations/edit/event_hub';
import axios from '~/lib/utils/axios_utils';
import toast from '~/vue_shared/plugins/global_toast';
+import {
+ I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE,
+ I18N_SUCCESSFUL_CONNECTION_MESSAGE,
+ I18N_DEFAULT_ERROR_MESSAGE,
+ GET_JIRA_ISSUE_TYPES_EVENT,
+ TOGGLE_INTEGRATION_EVENT,
+ TEST_INTEGRATION_EVENT,
+ SAVE_INTEGRATION_EVENT,
+} from '~/integrations/constants';
+import waitForPromises from 'helpers/wait_for_promises';
jest.mock('~/vue_shared/plugins/global_toast');
+jest.mock('lodash/delay', () => (callback) => callback());
+
+const FIXTURE = 'services/edit_service.html';
describe('IntegrationSettingsForm', () => {
- const FIXTURE = 'services/edit_service.html';
+ let integrationSettingsForm;
+
+ const mockStoreDispatch = () => jest.spyOn(integrationSettingsForm.vue.$store, 'dispatch');
beforeEach(() => {
loadFixtures(FIXTURE);
+
+ integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
+ integrationSettingsForm.init();
});
describe('constructor', () => {
- let integrationSettingsForm;
-
- beforeEach(() => {
- integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
- jest.spyOn(integrationSettingsForm, 'init').mockImplementation(() => {});
- });
-
it('should initialize form element refs on class object', () => {
- // Form Reference
expect(integrationSettingsForm.$form).toBeDefined();
expect(integrationSettingsForm.$form.nodeName).toBe('FORM');
expect(integrationSettingsForm.formActive).toBeDefined();
@@ -32,180 +43,206 @@ describe('IntegrationSettingsForm', () => {
});
});
- describe('toggleServiceState', () => {
- let integrationSettingsForm;
+ describe('event handling', () => {
+ let mockAxios;
beforeEach(() => {
- integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
- });
-
- it('should remove `novalidate` attribute to form when called with `true`', () => {
- integrationSettingsForm.formActive = true;
- integrationSettingsForm.toggleServiceState();
-
- expect(integrationSettingsForm.$form.getAttribute('novalidate')).toBe(null);
- });
-
- it('should set `novalidate` attribute to form when called with `false`', () => {
- integrationSettingsForm.formActive = false;
- integrationSettingsForm.toggleServiceState();
-
- expect(integrationSettingsForm.$form.getAttribute('novalidate')).toBeDefined();
- });
- });
-
- describe('testSettings', () => {
- let integrationSettingsForm;
- let formData;
- let mock;
-
- beforeEach(() => {
- mock = new MockAdaptor(axios);
-
+ mockAxios = new MockAdaptor(axios);
jest.spyOn(axios, 'put');
-
- integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
- integrationSettingsForm.init();
-
- formData = new FormData(integrationSettingsForm.$form);
});
afterEach(() => {
- mock.restore();
+ mockAxios.restore();
+ eventHub.dispose(); // clear event hub handlers
});
- it('should make an ajax request with provided `formData`', async () => {
- await integrationSettingsForm.testSettings(formData);
+ describe('when event hub receives `TOGGLE_INTEGRATION_EVENT`', () => {
+ it('should remove `novalidate` attribute to form when called with `true`', () => {
+ eventHub.$emit(TOGGLE_INTEGRATION_EVENT, true);
- expect(axios.put).toHaveBeenCalledWith(integrationSettingsForm.testEndPoint, formData);
- });
+ expect(integrationSettingsForm.$form.getAttribute('novalidate')).toBe(null);
+ });
- it('should show success message if test is successful', async () => {
- jest.spyOn(integrationSettingsForm.$form, 'submit').mockImplementation(() => {});
+ it('should set `novalidate` attribute to form when called with `false`', () => {
+ eventHub.$emit(TOGGLE_INTEGRATION_EVENT, false);
- mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
- error: false,
+ expect(integrationSettingsForm.$form.getAttribute('novalidate')).toBe('novalidate');
});
+ });
- await integrationSettingsForm.testSettings(formData);
+ describe('when event hub receives `TEST_INTEGRATION_EVENT`', () => {
+ describe('when form is valid', () => {
+ beforeEach(() => {
+ jest.spyOn(integrationSettingsForm.$form, 'checkValidity').mockReturnValue(true);
+ });
- expect(toast).toHaveBeenCalledWith('Connection successful.');
- });
+ it('should make an ajax request with provided `formData`', async () => {
+ eventHub.$emit(TEST_INTEGRATION_EVENT);
+ await waitForPromises();
- it('should show error message if ajax request responds with test error', async () => {
- const errorMessage = 'Test failed.';
- const serviceResponse = 'some error';
+ expect(axios.put).toHaveBeenCalledWith(
+ integrationSettingsForm.testEndPoint,
+ new FormData(integrationSettingsForm.$form),
+ );
+ });
- mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
- error: true,
- message: errorMessage,
- service_response: serviceResponse,
- test_failed: false,
- });
+ it('should show success message if test is successful', async () => {
+ jest.spyOn(integrationSettingsForm.$form, 'submit').mockImplementation(() => {});
- await integrationSettingsForm.testSettings(formData);
+ mockAxios.onPut(integrationSettingsForm.testEndPoint).reply(200, {
+ error: false,
+ });
- expect(toast).toHaveBeenCalledWith(`${errorMessage} ${serviceResponse}`);
- });
+ eventHub.$emit(TEST_INTEGRATION_EVENT);
+ await waitForPromises();
- it('should show error message if ajax request failed', async () => {
- const errorMessage = 'Something went wrong on our end.';
+ expect(toast).toHaveBeenCalledWith(I18N_SUCCESSFUL_CONNECTION_MESSAGE);
+ });
- mock.onPut(integrationSettingsForm.testEndPoint).networkError();
+ it('should show error message if ajax request responds with test error', async () => {
+ const errorMessage = 'Test failed.';
+ const serviceResponse = 'some error';
- await integrationSettingsForm.testSettings(formData);
+ mockAxios.onPut(integrationSettingsForm.testEndPoint).reply(200, {
+ error: true,
+ message: errorMessage,
+ service_response: serviceResponse,
+ test_failed: false,
+ });
- expect(toast).toHaveBeenCalledWith(errorMessage);
- });
+ eventHub.$emit(TEST_INTEGRATION_EVENT);
+ await waitForPromises();
- it('should always dispatch `setIsTesting` with `false` once request is completed', async () => {
- const dispatchSpy = jest.fn();
+ expect(toast).toHaveBeenCalledWith(`${errorMessage} ${serviceResponse}`);
+ });
- mock.onPut(integrationSettingsForm.testEndPoint).networkError();
+ it('should show error message if ajax request failed', async () => {
+ mockAxios.onPut(integrationSettingsForm.testEndPoint).networkError();
- integrationSettingsForm.vue.$store = { dispatch: dispatchSpy };
+ eventHub.$emit(TEST_INTEGRATION_EVENT);
+ await waitForPromises();
- await integrationSettingsForm.testSettings(formData);
+ expect(toast).toHaveBeenCalledWith(I18N_DEFAULT_ERROR_MESSAGE);
+ });
- expect(dispatchSpy).toHaveBeenCalledWith('setIsTesting', false);
- });
- });
+ it('should always dispatch `setIsTesting` with `false` once request is completed', async () => {
+ const dispatchSpy = mockStoreDispatch();
+ mockAxios.onPut(integrationSettingsForm.testEndPoint).networkError();
- describe('getJiraIssueTypes', () => {
- let integrationSettingsForm;
- let formData;
- let mock;
+ eventHub.$emit(TEST_INTEGRATION_EVENT);
+ await waitForPromises();
- beforeEach(() => {
- mock = new MockAdaptor(axios);
+ expect(dispatchSpy).toHaveBeenCalledWith('setIsTesting', false);
+ });
+ });
- jest.spyOn(axios, 'put');
+ describe('when form is invalid', () => {
+ beforeEach(() => {
+ jest.spyOn(integrationSettingsForm.$form, 'checkValidity').mockReturnValue(false);
+ jest.spyOn(integrationSettingsForm, 'testSettings');
+ });
- integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
- integrationSettingsForm.init();
+ it('should dispatch `setIsTesting` with `false` and not call `testSettings`', async () => {
+ const dispatchSpy = mockStoreDispatch();
- formData = new FormData(integrationSettingsForm.$form);
- });
+ eventHub.$emit(TEST_INTEGRATION_EVENT);
+ await waitForPromises();
- afterEach(() => {
- mock.restore();
+ expect(dispatchSpy).toHaveBeenCalledWith('setIsTesting', false);
+ expect(integrationSettingsForm.testSettings).not.toHaveBeenCalled();
+ });
+ });
});
- it('should always dispatch `requestJiraIssueTypes`', async () => {
- const dispatchSpy = jest.fn();
-
- mock.onPut(integrationSettingsForm.testEndPoint).networkError();
+ describe('when event hub receives `GET_JIRA_ISSUE_TYPES_EVENT`', () => {
+ it('should always dispatch `requestJiraIssueTypes`', () => {
+ const dispatchSpy = mockStoreDispatch();
+ mockAxios.onPut(integrationSettingsForm.testEndPoint).networkError();
- integrationSettingsForm.vue.$store = { dispatch: dispatchSpy };
+ eventHub.$emit(GET_JIRA_ISSUE_TYPES_EVENT);
- await integrationSettingsForm.getJiraIssueTypes();
+ expect(dispatchSpy).toHaveBeenCalledWith('requestJiraIssueTypes');
+ });
- expect(dispatchSpy).toHaveBeenCalledWith('requestJiraIssueTypes');
- });
+ it('should make an ajax request with provided `formData`', () => {
+ eventHub.$emit(GET_JIRA_ISSUE_TYPES_EVENT);
- it('should make an ajax request with provided `formData`', async () => {
- await integrationSettingsForm.getJiraIssueTypes(formData);
+ expect(axios.put).toHaveBeenCalledWith(
+ integrationSettingsForm.testEndPoint,
+ new FormData(integrationSettingsForm.$form),
+ );
+ });
- expect(axios.put).toHaveBeenCalledWith(integrationSettingsForm.testEndPoint, formData);
- });
+ it('should dispatch `receiveJiraIssueTypesSuccess` with the correct payload if ajax request is successful', async () => {
+ const dispatchSpy = mockStoreDispatch();
+ const mockData = ['ISSUE', 'EPIC'];
+ mockAxios.onPut(integrationSettingsForm.testEndPoint).reply(200, {
+ error: false,
+ issuetypes: mockData,
+ });
- it('should dispatch `receiveJiraIssueTypesSuccess` with the correct payload if ajax request is successful', async () => {
- const mockData = ['ISSUE', 'EPIC'];
- const dispatchSpy = jest.fn();
+ eventHub.$emit(GET_JIRA_ISSUE_TYPES_EVENT);
+ await waitForPromises();
- mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
- error: false,
- issuetypes: mockData,
+ expect(dispatchSpy).toHaveBeenCalledWith('receiveJiraIssueTypesSuccess', mockData);
});
- integrationSettingsForm.vue.$store = { dispatch: dispatchSpy };
-
- await integrationSettingsForm.getJiraIssueTypes(formData);
+ it.each(['Custom error message here', undefined])(
+ 'should dispatch "receiveJiraIssueTypesError" with a message if the backend responds with error',
+ async (responseErrorMessage) => {
+ const dispatchSpy = mockStoreDispatch();
+
+ const expectedErrorMessage =
+ responseErrorMessage || I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE;
+ mockAxios.onPut(integrationSettingsForm.testEndPoint).reply(200, {
+ error: true,
+ message: responseErrorMessage,
+ });
+
+ eventHub.$emit(GET_JIRA_ISSUE_TYPES_EVENT);
+ await waitForPromises();
+
+ expect(dispatchSpy).toHaveBeenCalledWith(
+ 'receiveJiraIssueTypesError',
+ expectedErrorMessage,
+ );
+ },
+ );
+ });
+
+ describe('when event hub receives `SAVE_INTEGRATION_EVENT`', () => {
+ describe('when form is valid', () => {
+ beforeEach(() => {
+ jest.spyOn(integrationSettingsForm.$form, 'checkValidity').mockReturnValue(true);
+ jest.spyOn(integrationSettingsForm.$form, 'submit');
+ });
- expect(dispatchSpy).toHaveBeenCalledWith('receiveJiraIssueTypesSuccess', mockData);
- });
+ it('should submit the form', async () => {
+ eventHub.$emit(SAVE_INTEGRATION_EVENT);
+ await waitForPromises();
- 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();
+ expect(integrationSettingsForm.$form.submit).toHaveBeenCalled();
+ expect(integrationSettingsForm.$form.submit).toHaveBeenCalledTimes(1);
+ });
+ });
- mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
- error: true,
- message: responseErrorMessage,
+ describe('when form is invalid', () => {
+ beforeEach(() => {
+ jest.spyOn(integrationSettingsForm.$form, 'checkValidity').mockReturnValue(false);
+ jest.spyOn(integrationSettingsForm.$form, 'submit');
});
- integrationSettingsForm.vue.$store = { dispatch: dispatchSpy };
+ it('should dispatch `setIsSaving` with `false` and not submit form', async () => {
+ const dispatchSpy = mockStoreDispatch();
- await integrationSettingsForm.getJiraIssueTypes(formData);
+ eventHub.$emit(SAVE_INTEGRATION_EVENT);
- expect(dispatchSpy).toHaveBeenCalledWith(
- 'receiveJiraIssueTypesError',
- expectedErrorMessage,
- );
- },
- );
+ await waitForPromises();
+
+ expect(dispatchSpy).toHaveBeenCalledWith('setIsSaving', false);
+ expect(integrationSettingsForm.$form.submit).not.toHaveBeenCalled();
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/invite_members/components/confetti_spec.js b/spec/frontend/invite_members/components/confetti_spec.js
new file mode 100644
index 00000000000..2f361f1dc1e
--- /dev/null
+++ b/spec/frontend/invite_members/components/confetti_spec.js
@@ -0,0 +1,28 @@
+import { shallowMount } from '@vue/test-utils';
+import confetti from 'canvas-confetti';
+import Confetti from '~/invite_members/components/confetti.vue';
+
+jest.mock('canvas-confetti', () => ({
+ create: jest.fn(),
+}));
+
+let wrapper;
+
+const createComponent = () => {
+ wrapper = shallowMount(Confetti);
+};
+
+afterEach(() => {
+ wrapper.destroy();
+});
+
+describe('Confetti', () => {
+ it('initiates confetti', () => {
+ const basicCannon = jest.spyOn(Confetti.methods, 'basicCannon').mockImplementation(() => {});
+
+ createComponent();
+
+ expect(confetti.create).toHaveBeenCalled();
+ expect(basicCannon).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/invite_members/components/invite_members_modal_spec.js b/spec/frontend/invite_members/components/invite_members_modal_spec.js
index 8c3c549a5eb..5be79004640 100644
--- a/spec/frontend/invite_members/components/invite_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js
@@ -15,17 +15,34 @@ import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
import ExperimentTracking from '~/experimentation/experiment_tracking';
import InviteMembersModal from '~/invite_members/components/invite_members_modal.vue';
+import ModalConfetti from '~/invite_members/components/confetti.vue';
import MembersTokenSelect from '~/invite_members/components/members_token_select.vue';
-import { INVITE_MEMBERS_IN_COMMENT, MEMBER_AREAS_OF_FOCUS } from '~/invite_members/constants';
+import {
+ INVITE_MEMBERS_IN_COMMENT,
+ MEMBER_AREAS_OF_FOCUS,
+ INVITE_MEMBERS_FOR_TASK,
+ CANCEL_BUTTON_TEXT,
+ INVITE_BUTTON_TEXT,
+ MEMBERS_MODAL_CELEBRATE_INTRO,
+ MEMBERS_MODAL_CELEBRATE_TITLE,
+ MEMBERS_MODAL_DEFAULT_TITLE,
+ MEMBERS_PLACEHOLDER,
+ MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT,
+} from '~/invite_members/constants';
import eventHub from '~/invite_members/event_hub';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
+import { getParameterValues } from '~/lib/utils/url_utility';
import { apiPaths, membersApiResponse, invitationsApiResponse } from '../mock_data/api_responses';
let wrapper;
let mock;
jest.mock('~/experimentation/experiment_tracking');
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ getParameterValues: jest.fn(() => []),
+}));
const id = '1';
const name = 'test name';
@@ -40,6 +57,15 @@ const areasOfFocusOptions = [
{ text: 'area1', value: 'area1' },
{ text: 'area2', value: 'area2' },
];
+const tasksToBeDoneOptions = [
+ { text: 'First task', value: 'first' },
+ { text: 'Second task', value: 'second' },
+];
+const newProjectPath = 'projects/new';
+const projects = [
+ { text: 'First project', value: '1' },
+ { text: 'Second project', value: '2' },
+];
const user1 = { id: 1, name: 'Name One', username: 'one_1', avatar_url: '' };
const user2 = { id: 2, name: 'Name Two', username: 'one_2', avatar_url: '' };
@@ -56,9 +82,13 @@ const user4 = {
avatar_url: '',
};
const sharedGroup = { id: '981' };
+const GlEmoji = { template: '<img/>' };
const createComponent = (data = {}, props = {}) => {
wrapper = shallowMountExtended(InviteMembersModal, {
+ provide: {
+ newProjectPath,
+ },
propsData: {
id,
name,
@@ -68,6 +98,8 @@ const createComponent = (data = {}, props = {}) => {
areasOfFocusOptions,
defaultAccessLevel,
noSelectionAreasOfFocus,
+ tasksToBeDoneOptions,
+ projects,
helpLink,
...props,
},
@@ -81,6 +113,7 @@ const createComponent = (data = {}, props = {}) => {
}),
GlDropdown: true,
GlDropdownItem: true,
+ GlEmoji,
GlSprintf,
GlFormGroup: stubComponent(GlFormGroup, {
props: ['state', 'invalidFeedback', 'description'],
@@ -131,6 +164,11 @@ describe('InviteMembersModal', () => {
const membersFormGroupDescription = () => findMembersFormGroup().props('description');
const findMembersSelect = () => wrapper.findComponent(MembersTokenSelect);
const findAreaofFocusCheckBoxGroup = () => wrapper.findComponent(GlFormCheckboxGroup);
+ const findTasksToBeDone = () => wrapper.findByTestId('invite-members-modal-tasks-to-be-done');
+ const findTasks = () => wrapper.findByTestId('invite-members-modal-tasks');
+ const findProjectSelect = () => wrapper.findByTestId('invite-members-modal-project-select');
+ const findNoProjectsAlert = () => wrapper.findByTestId('invite-members-modal-no-projects-alert');
+ const findCelebrationEmoji = () => wrapper.findComponent(GlModal).find(GlEmoji);
describe('rendering the modal', () => {
beforeEach(() => {
@@ -138,15 +176,15 @@ describe('InviteMembersModal', () => {
});
it('renders the modal with the correct title', () => {
- expect(wrapper.findComponent(GlModal).props('title')).toBe('Invite members');
+ expect(wrapper.findComponent(GlModal).props('title')).toBe(MEMBERS_MODAL_DEFAULT_TITLE);
});
it('renders the Cancel button text correctly', () => {
- expect(findCancelButton().text()).toBe('Cancel');
+ expect(findCancelButton().text()).toBe(CANCEL_BUTTON_TEXT);
});
it('renders the Invite button text correctly', () => {
- expect(findInviteButton().text()).toBe('Invite');
+ expect(findInviteButton().text()).toBe(INVITE_BUTTON_TEXT);
});
it('renders the Invite button modal without isLoading', () => {
@@ -171,7 +209,7 @@ describe('InviteMembersModal', () => {
describe('rendering the access expiration date field', () => {
it('renders the datepicker', () => {
- expect(findDatepicker()).toExist();
+ expect(findDatepicker().exists()).toBe(true);
});
});
});
@@ -191,14 +229,164 @@ describe('InviteMembersModal', () => {
});
});
+ describe('rendering the tasks to be done', () => {
+ const setupComponent = (
+ extraData = {},
+ props = {},
+ urlParameter = ['invite_members_for_task'],
+ ) => {
+ const data = {
+ selectedAccessLevel: 30,
+ selectedTasksToBeDone: ['ci', 'code'],
+ ...extraData,
+ };
+ getParameterValues.mockImplementation(() => urlParameter);
+ createComponent(data, props);
+ };
+
+ afterAll(() => {
+ getParameterValues.mockImplementation(() => []);
+ });
+
+ it('renders the tasks to be done', () => {
+ setupComponent();
+
+ expect(findTasksToBeDone().exists()).toBe(true);
+ });
+
+ describe('when the selected access level is lower than 30', () => {
+ it('does not render the tasks to be done', () => {
+ setupComponent({ selectedAccessLevel: 20 });
+
+ expect(findTasksToBeDone().exists()).toBe(false);
+ });
+ });
+
+ describe('when the url does not contain the parameter `open_modal=invite_members_for_task`', () => {
+ it('does not render the tasks to be done', () => {
+ setupComponent({}, {}, []);
+
+ expect(findTasksToBeDone().exists()).toBe(false);
+ });
+ });
+
+ describe('rendering the tasks', () => {
+ it('renders the tasks', () => {
+ setupComponent();
+
+ expect(findTasks().exists()).toBe(true);
+ });
+
+ it('does not render an alert', () => {
+ setupComponent();
+
+ expect(findNoProjectsAlert().exists()).toBe(false);
+ });
+
+ describe('when there are no projects passed in the data', () => {
+ it('does not render the tasks', () => {
+ setupComponent({}, { projects: [] });
+
+ expect(findTasks().exists()).toBe(false);
+ });
+
+ it('renders an alert with a link to the new projects path', () => {
+ setupComponent({}, { projects: [] });
+
+ expect(findNoProjectsAlert().exists()).toBe(true);
+ expect(findNoProjectsAlert().findComponent(GlLink).attributes('href')).toBe(
+ newProjectPath,
+ );
+ });
+ });
+ });
+
+ describe('rendering the project dropdown', () => {
+ it('renders the project select', () => {
+ setupComponent();
+
+ expect(findProjectSelect().exists()).toBe(true);
+ });
+
+ describe('when the modal is shown for a project', () => {
+ it('does not render the project select', () => {
+ setupComponent({}, { isProject: true });
+
+ expect(findProjectSelect().exists()).toBe(false);
+ });
+ });
+
+ describe('when no tasks are selected', () => {
+ it('does not render the project select', () => {
+ setupComponent({ selectedTasksToBeDone: [] });
+
+ expect(findProjectSelect().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('tracking events', () => {
+ it('tracks the view for invite_members_for_task', () => {
+ setupComponent();
+
+ expect(ExperimentTracking).toHaveBeenCalledWith(INVITE_MEMBERS_FOR_TASK.name);
+ expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(
+ INVITE_MEMBERS_FOR_TASK.view,
+ );
+ });
+
+ it('tracks the submit for invite_members_for_task', () => {
+ setupComponent();
+ clickInviteButton();
+
+ expect(ExperimentTracking).toHaveBeenCalledWith(INVITE_MEMBERS_FOR_TASK.name, {
+ label: 'selected_tasks_to_be_done',
+ property: 'ci,code',
+ });
+ expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(
+ INVITE_MEMBERS_FOR_TASK.submit,
+ );
+ });
+ });
+ });
+
describe('displaying the correct introText and form group description', () => {
describe('when inviting to a project', () => {
describe('when inviting members', () => {
- it('includes the correct invitee, type, and formatted name', () => {
+ beforeEach(() => {
createInviteMembersToProjectWrapper();
+ });
+ it('renders the modal without confetti', () => {
+ expect(wrapper.findComponent(ModalConfetti).exists()).toBe(false);
+ });
+
+ it('includes the correct invitee, type, and formatted name', () => {
expect(findIntroText()).toBe("You're inviting members to the test name project.");
- expect(membersFormGroupDescription()).toBe('Select members or type email addresses');
+ expect(findCelebrationEmoji().exists()).toBe(false);
+ expect(membersFormGroupDescription()).toBe(MEMBERS_PLACEHOLDER);
+ });
+ });
+
+ describe('when inviting members with celebration', () => {
+ beforeEach(() => {
+ createComponent({ mode: 'celebrate', inviteeType: 'members' }, { isProject: true });
+ });
+
+ it('renders the modal with confetti', () => {
+ expect(wrapper.findComponent(ModalConfetti).exists()).toBe(true);
+ });
+
+ it('renders the modal with the correct title', () => {
+ expect(wrapper.findComponent(GlModal).props('title')).toBe(MEMBERS_MODAL_CELEBRATE_TITLE);
+ });
+
+ it('includes the correct celebration text and emoji', () => {
+ expect(findIntroText()).toBe(
+ `${MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT} ${MEMBERS_MODAL_CELEBRATE_INTRO}`,
+ );
+ expect(findCelebrationEmoji().exists()).toBe(true);
+ expect(membersFormGroupDescription()).toBe(MEMBERS_PLACEHOLDER);
});
});
@@ -218,7 +406,7 @@ describe('InviteMembersModal', () => {
createInviteMembersToGroupWrapper();
expect(findIntroText()).toBe("You're inviting members to the test name group.");
- expect(membersFormGroupDescription()).toBe('Select members or type email addresses');
+ expect(membersFormGroupDescription()).toBe(MEMBERS_PLACEHOLDER);
});
});
@@ -267,6 +455,8 @@ describe('InviteMembersModal', () => {
invite_source: inviteSource,
format: 'json',
areas_of_focus: noSelectionAreasOfFocus,
+ tasks_to_be_done: [],
+ tasks_project_id: '',
};
describe('when member is added successfully', () => {
@@ -448,6 +638,8 @@ describe('InviteMembersModal', () => {
email: 'email@example.com',
invite_source: inviteSource,
areas_of_focus: noSelectionAreasOfFocus,
+ tasks_to_be_done: [],
+ tasks_project_id: '',
format: 'json',
};
@@ -576,6 +768,8 @@ describe('InviteMembersModal', () => {
invite_source: inviteSource,
areas_of_focus: noSelectionAreasOfFocus,
format: 'json',
+ tasks_to_be_done: [],
+ tasks_project_id: '',
};
const emailPostData = { ...postData, email: 'email@example.com' };
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 b2ebb9e4a47..3fce23f854c 100644
--- a/spec/frontend/invite_members/components/invite_members_trigger_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_trigger_spec.js
@@ -1,8 +1,9 @@
-import { GlButton, GlLink } from '@gitlab/ui';
+import { GlButton, GlLink, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import ExperimentTracking from '~/experimentation/experiment_tracking';
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
import eventHub from '~/invite_members/event_hub';
+import { TRIGGER_ELEMENT_BUTTON, TRIGGER_ELEMENT_SIDE_NAV } from '~/invite_members/constants';
jest.mock('~/experimentation/experiment_tracking');
@@ -15,6 +16,7 @@ let findButton;
const triggerComponent = {
button: GlButton,
anchor: GlLink,
+ 'side-nav': GlLink,
};
const createComponent = (props = {}) => {
@@ -27,9 +29,23 @@ const createComponent = (props = {}) => {
});
};
-describe.each(['button', 'anchor'])('with triggerElement as %s', (triggerElement) => {
- triggerProps = { triggerElement, triggerSource };
- findButton = () => wrapper.findComponent(triggerComponent[triggerElement]);
+const triggerItems = [
+ {
+ triggerElement: TRIGGER_ELEMENT_BUTTON,
+ },
+ {
+ triggerElement: 'anchor',
+ },
+ {
+ triggerElement: TRIGGER_ELEMENT_SIDE_NAV,
+ icon: 'plus',
+ },
+];
+
+describe.each(triggerItems)('with triggerElement as %s', (triggerItem) => {
+ triggerProps = { ...triggerItem, triggerSource };
+
+ findButton = () => wrapper.findComponent(triggerComponent[triggerItem.triggerElement]);
afterEach(() => {
wrapper.destroy();
@@ -91,3 +107,14 @@ describe.each(['button', 'anchor'])('with triggerElement as %s', (triggerElement
});
});
});
+
+describe('side-nav with icon', () => {
+ it('includes the specified icon with correct size when triggerElement is link', () => {
+ const findIcon = () => wrapper.findComponent(GlIcon);
+
+ createComponent({ triggerElement: TRIGGER_ELEMENT_SIDE_NAV, icon: 'plus' });
+
+ expect(findIcon().exists()).toBe(true);
+ expect(findIcon().props('name')).toBe('plus');
+ });
+});
diff --git a/spec/frontend/issuable/components/csv_import_modal_spec.js b/spec/frontend/issuable/components/csv_import_modal_spec.js
index 307323ef07a..f4636fd7e6a 100644
--- a/spec/frontend/issuable/components/csv_import_modal_spec.js
+++ b/spec/frontend/issuable/components/csv_import_modal_spec.js
@@ -1,7 +1,8 @@
-import { GlButton, GlModal } from '@gitlab/ui';
+import { GlModal } from '@gitlab/ui';
import { stubComponent } from 'helpers/stub_component';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import CsvImportModal from '~/issuable/components/csv_import_modal.vue';
+import { __ } from '~/locale';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
@@ -36,7 +37,6 @@ describe('CsvImportModal', () => {
});
const findModal = () => wrapper.findComponent(GlModal);
- const findPrimaryButton = () => wrapper.findComponent(GlButton);
const findForm = () => wrapper.find('form');
const findFileInput = () => wrapper.findByLabelText('Upload CSV file');
const findAuthenticityToken = () => new FormData(findForm().element).get('authenticity_token');
@@ -64,11 +64,11 @@ describe('CsvImportModal', () => {
expect(findForm().exists()).toBe(true);
expect(findForm().attributes('action')).toBe(importCsvIssuesPath);
expect(findAuthenticityToken()).toBe('mock-csrf-token');
- expect(findFileInput()).toExist();
+ expect(findFileInput().exists()).toBe(true);
});
it('displays the correct primary button action text', () => {
- expect(findPrimaryButton()).toExist();
+ expect(findModal().props('actionPrimary')).toEqual({ text: __('Import issues') });
});
it('submits the form when the primary action is clicked', () => {
diff --git a/spec/frontend/issue_show/components/app_spec.js b/spec/frontend/issue_show/components/app_spec.js
index bd05cb1ac5a..e32215b4aa6 100644
--- a/spec/frontend/issue_show/components/app_spec.js
+++ b/spec/frontend/issue_show/components/app_spec.js
@@ -8,7 +8,7 @@ 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 { IssuableStatus, IssuableStatusText, POLLING_DELAY } 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';
@@ -643,4 +643,40 @@ describe('Issuable output', () => {
});
});
});
+
+ describe('taskListUpdateStarted', () => {
+ it('stops polling', () => {
+ jest.spyOn(wrapper.vm.poll, 'stop');
+
+ wrapper.vm.taskListUpdateStarted();
+
+ expect(wrapper.vm.poll.stop).toHaveBeenCalled();
+ });
+ });
+
+ describe('taskListUpdateSucceeded', () => {
+ it('enables polling', () => {
+ jest.spyOn(wrapper.vm.poll, 'enable');
+ jest.spyOn(wrapper.vm.poll, 'makeDelayedRequest');
+
+ wrapper.vm.taskListUpdateSucceeded();
+
+ expect(wrapper.vm.poll.enable).toHaveBeenCalled();
+ expect(wrapper.vm.poll.makeDelayedRequest).toHaveBeenCalledWith(POLLING_DELAY);
+ });
+ });
+
+ describe('taskListUpdateFailed', () => {
+ it('enables polling and calls updateStoreState', () => {
+ jest.spyOn(wrapper.vm.poll, 'enable');
+ jest.spyOn(wrapper.vm.poll, 'makeDelayedRequest');
+ jest.spyOn(wrapper.vm, 'updateStoreState');
+
+ wrapper.vm.taskListUpdateFailed();
+
+ expect(wrapper.vm.poll.enable).toHaveBeenCalled();
+ expect(wrapper.vm.poll.makeDelayedRequest).toHaveBeenCalledWith(POLLING_DELAY);
+ expect(wrapper.vm.updateStoreState).toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/issue_show/components/description_spec.js b/spec/frontend/issue_show/components/description_spec.js
index cdf06ecc31f..bdcc82cab81 100644
--- a/spec/frontend/issue_show/components/description_spec.js
+++ b/spec/frontend/issue_show/components/description_spec.js
@@ -114,6 +114,8 @@ describe('Description component', () => {
dataType: 'issuableType',
fieldName: 'description',
selector: '.detail-page-description',
+ onUpdate: expect.any(Function),
+ onSuccess: expect.any(Function),
onError: expect.any(Function),
lockVersion: 0,
});
@@ -150,6 +152,26 @@ describe('Description component', () => {
});
});
+ describe('taskListUpdateStarted', () => {
+ it('emits event to parent', () => {
+ const spy = jest.spyOn(vm, '$emit');
+
+ vm.taskListUpdateStarted();
+
+ expect(spy).toHaveBeenCalledWith('taskListUpdateStarted');
+ });
+ });
+
+ describe('taskListUpdateSuccess', () => {
+ it('emits event to parent', () => {
+ const spy = jest.spyOn(vm, '$emit');
+
+ vm.taskListUpdateSuccess();
+
+ expect(spy).toHaveBeenCalledWith('taskListUpdateSucceeded');
+ });
+ });
+
describe('taskListUpdateError', () => {
it('should create flash notification and emit an event to parent', () => {
const msg =
diff --git a/spec/frontend/issue_show/components/fields/type_spec.js b/spec/frontend/issue_show/components/fields/type_spec.js
index fac745716d7..95ae6f37877 100644
--- a/spec/frontend/issue_show/components/fields/type_spec.js
+++ b/spec/frontend/issue_show/components/fields/type_spec.js
@@ -39,7 +39,7 @@ describe('Issue type field component', () => {
const findTypeFromDropDownItemIconAt = (at) =>
findTypeFromDropDownItems().at(at).findComponent(GlIcon);
- const createComponent = ({ data } = {}) => {
+ const createComponent = ({ data } = {}, provide) => {
fakeApollo = createMockApollo([], mockResolvers);
wrapper = shallowMount(IssueTypeField, {
@@ -51,6 +51,10 @@ describe('Issue type field component', () => {
...data,
};
},
+ provide: {
+ canCreateIncident: true,
+ ...provide,
+ },
});
};
@@ -92,5 +96,25 @@ describe('Issue type field component', () => {
await wrapper.vm.$nextTick();
expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.incident);
});
+
+ describe('when user is a guest', () => {
+ it('hides the incident type from the dropdown', async () => {
+ createComponent({}, { canCreateIncident: false, issueType: 'issue' });
+ await waitForPromises();
+
+ expect(findTypeFromDropDownItemAt(0).isVisible()).toBe(true);
+ expect(findTypeFromDropDownItemAt(1).isVisible()).toBe(false);
+ expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.issue);
+ });
+
+ it('and incident is selected, includes incident in the dropdown', async () => {
+ createComponent({}, { canCreateIncident: false, issueType: 'incident' });
+ await waitForPromises();
+
+ expect(findTypeFromDropDownItemAt(0).isVisible()).toBe(true);
+ expect(findTypeFromDropDownItemAt(1).isVisible()).toBe(true);
+ expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.incident);
+ });
+ });
});
});
diff --git a/spec/frontend/issues_list/components/issues_list_app_spec.js b/spec/frontend/issues_list/components/issues_list_app_spec.js
index 6b443062f12..3f52c7b4afe 100644
--- a/spec/frontend/issues_list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues_list/components/issues_list_app_spec.js
@@ -37,6 +37,7 @@ import {
TOKEN_TYPE_LABEL,
TOKEN_TYPE_MILESTONE,
TOKEN_TYPE_MY_REACTION,
+ TOKEN_TYPE_RELEASE,
TOKEN_TYPE_TYPE,
TOKEN_TYPE_WEIGHT,
urlSortParams,
@@ -581,6 +582,7 @@ describe('IssuesListApp component', () => {
{ type: TOKEN_TYPE_MILESTONE },
{ type: TOKEN_TYPE_LABEL },
{ type: TOKEN_TYPE_TYPE },
+ { type: TOKEN_TYPE_RELEASE },
{ type: TOKEN_TYPE_MY_REACTION },
{ type: TOKEN_TYPE_CONFIDENTIAL },
{ type: TOKEN_TYPE_ITERATION },
diff --git a/spec/frontend/issues_list/components/new_issue_dropdown_spec.js b/spec/frontend/issues_list/components/new_issue_dropdown_spec.js
index 1fcaa99cf5a..1c9a87e8af2 100644
--- a/spec/frontend/issues_list/components/new_issue_dropdown_spec.js
+++ b/spec/frontend/issues_list/components/new_issue_dropdown_spec.js
@@ -8,7 +8,7 @@ import { DASH_SCOPE, joinPaths } from '~/lib/utils/url_utility';
import {
emptySearchProjectsQueryResponse,
project1,
- project2,
+ project3,
searchProjectsQueryResponse,
} from '../mock_data';
@@ -72,7 +72,7 @@ describe('NewIssueDropdown component', () => {
expect(inputSpy).toHaveBeenCalledTimes(1);
});
- it('renders expected dropdown items', async () => {
+ it('renders projects with issues enabled', async () => {
wrapper = mountComponent({ mountFn: mount });
await showDropdown();
@@ -80,7 +80,7 @@ describe('NewIssueDropdown component', () => {
const listItems = wrapper.findAll('li');
expect(listItems.at(0).text()).toBe(project1.nameWithNamespace);
- expect(listItems.at(1).text()).toBe(project2.nameWithNamespace);
+ expect(listItems.at(1).text()).toBe(project3.nameWithNamespace);
});
it('renders `No matches found` when there are no matches', async () => {
diff --git a/spec/frontend/issues_list/mock_data.js b/spec/frontend/issues_list/mock_data.js
index 3be256d8094..19a8af4d9c2 100644
--- a/spec/frontend/issues_list/mock_data.js
+++ b/spec/frontend/issues_list/mock_data.js
@@ -95,16 +95,29 @@ export const locationSearch = [
'assignee_username[]=lisa',
'not[assignee_username][]=patty',
'not[assignee_username][]=selma',
+ 'milestone_title=season+3',
'milestone_title=season+4',
'not[milestone_title]=season+20',
+ 'not[milestone_title]=season+30',
'label_name[]=cartoon',
'label_name[]=tv',
'not[label_name][]=live action',
'not[label_name][]=drama',
+ 'release_tag=v3',
+ 'release_tag=v4',
+ 'not[release_tag]=v20',
+ 'not[release_tag]=v30',
+ 'type[]=issue',
+ 'type[]=feature',
+ 'not[type][]=bug',
+ 'not[type][]=incident',
'my_reaction_emoji=thumbsup',
- 'confidential=no',
+ 'not[my_reaction_emoji]=thumbsdown',
+ 'confidential=yes',
'iteration_id=4',
+ 'iteration_id=12',
'not[iteration_id]=20',
+ 'not[iteration_id]=42',
'epic_id=12',
'not[epic_id]=34',
'weight=1',
@@ -114,10 +127,10 @@ export const locationSearch = [
export const locationSearchWithSpecialValues = [
'assignee_id=123',
'assignee_username=bart',
- 'type[]=issue',
- 'type[]=incident',
'my_reaction_emoji=None',
'iteration_id=Current',
+ 'label_name[]=None',
+ 'release_tag=None',
'milestone_title=Upcoming',
'epic_id=None',
'weight=None',
@@ -130,16 +143,29 @@ export const filteredTokens = [
{ type: 'assignee_username', value: { data: 'lisa', operator: OPERATOR_IS } },
{ type: 'assignee_username', value: { data: 'patty', operator: OPERATOR_IS_NOT } },
{ type: 'assignee_username', value: { data: 'selma', operator: OPERATOR_IS_NOT } },
+ { type: 'milestone', value: { data: 'season 3', operator: OPERATOR_IS } },
{ type: 'milestone', value: { data: 'season 4', operator: OPERATOR_IS } },
{ type: 'milestone', value: { data: 'season 20', operator: OPERATOR_IS_NOT } },
+ { type: 'milestone', value: { data: 'season 30', operator: OPERATOR_IS_NOT } },
{ type: 'labels', value: { data: 'cartoon', operator: OPERATOR_IS } },
{ type: 'labels', value: { data: 'tv', operator: OPERATOR_IS } },
{ type: 'labels', value: { data: 'live action', operator: OPERATOR_IS_NOT } },
{ type: 'labels', value: { data: 'drama', operator: OPERATOR_IS_NOT } },
+ { type: 'release', value: { data: 'v3', operator: OPERATOR_IS } },
+ { type: 'release', value: { data: 'v4', operator: OPERATOR_IS } },
+ { type: 'release', value: { data: 'v20', operator: OPERATOR_IS_NOT } },
+ { type: 'release', value: { data: 'v30', operator: OPERATOR_IS_NOT } },
+ { type: 'type', value: { data: 'issue', operator: OPERATOR_IS } },
+ { type: 'type', value: { data: 'feature', operator: OPERATOR_IS } },
+ { type: 'type', value: { data: 'bug', operator: OPERATOR_IS_NOT } },
+ { type: 'type', value: { data: 'incident', operator: OPERATOR_IS_NOT } },
{ type: 'my_reaction_emoji', value: { data: 'thumbsup', operator: OPERATOR_IS } },
- { type: 'confidential', value: { data: 'no', operator: OPERATOR_IS } },
+ { type: 'my_reaction_emoji', value: { data: 'thumbsdown', operator: OPERATOR_IS_NOT } },
+ { type: 'confidential', value: { data: 'yes', operator: OPERATOR_IS } },
{ type: 'iteration', value: { data: '4', operator: OPERATOR_IS } },
+ { type: 'iteration', value: { data: '12', operator: OPERATOR_IS } },
{ type: 'iteration', value: { data: '20', operator: OPERATOR_IS_NOT } },
+ { type: 'iteration', value: { data: '42', operator: OPERATOR_IS_NOT } },
{ type: 'epic_id', value: { data: '12', operator: OPERATOR_IS } },
{ type: 'epic_id', value: { data: '34', operator: OPERATOR_IS_NOT } },
{ type: 'weight', value: { data: '1', operator: OPERATOR_IS } },
@@ -151,10 +177,10 @@ export const filteredTokens = [
export const filteredTokensWithSpecialValues = [
{ type: 'assignee_username', value: { data: '123', operator: OPERATOR_IS } },
{ type: 'assignee_username', value: { data: 'bart', operator: OPERATOR_IS } },
- { type: 'type', value: { data: 'issue', operator: OPERATOR_IS } },
- { type: 'type', value: { data: 'incident', operator: OPERATOR_IS } },
{ type: 'my_reaction_emoji', value: { data: 'None', operator: OPERATOR_IS } },
{ type: 'iteration', value: { data: 'Current', operator: OPERATOR_IS } },
+ { type: 'labels', value: { data: 'None', operator: OPERATOR_IS } },
+ { type: 'release', value: { data: 'None', operator: OPERATOR_IS } },
{ type: 'milestone', value: { data: 'Upcoming', operator: OPERATOR_IS } },
{ type: 'epic_id', value: { data: 'None', operator: OPERATOR_IS } },
{ type: 'weight', value: { data: 'None', operator: OPERATOR_IS } },
@@ -163,19 +189,24 @@ export const filteredTokensWithSpecialValues = [
export const apiParams = {
authorUsername: 'homer',
assigneeUsernames: ['bart', 'lisa'],
- milestoneTitle: 'season 4',
+ milestoneTitle: ['season 3', 'season 4'],
labelName: ['cartoon', 'tv'],
+ releaseTag: ['v3', 'v4'],
+ types: ['ISSUE', 'FEATURE'],
myReactionEmoji: 'thumbsup',
- confidential: 'no',
- iterationId: '4',
+ confidential: true,
+ iterationId: ['4', '12'],
epicId: '12',
weight: '1',
not: {
authorUsername: 'marge',
assigneeUsernames: ['patty', 'selma'],
- milestoneTitle: 'season 20',
+ milestoneTitle: ['season 20', 'season 30'],
labelName: ['live action', 'drama'],
- iterationId: '20',
+ releaseTag: ['v20', 'v30'],
+ types: ['BUG', 'INCIDENT'],
+ myReactionEmoji: 'thumbsdown',
+ iterationId: ['20', '42'],
epicId: '34',
weight: '3',
},
@@ -184,8 +215,9 @@ export const apiParams = {
export const apiParamsWithSpecialValues = {
assigneeId: '123',
assigneeUsernames: 'bart',
- types: ['ISSUE', 'INCIDENT'],
+ labelName: 'None',
myReactionEmoji: 'None',
+ releaseTagWildcardId: 'NONE',
iterationWildcardId: 'CURRENT',
milestoneWildcardId: 'UPCOMING',
epicId: 'None',
@@ -197,14 +229,19 @@ export const urlParams = {
'not[author_username]': 'marge',
'assignee_username[]': ['bart', 'lisa'],
'not[assignee_username][]': ['patty', 'selma'],
- milestone_title: 'season 4',
- 'not[milestone_title]': 'season 20',
+ milestone_title: ['season 3', 'season 4'],
+ 'not[milestone_title]': ['season 20', 'season 30'],
'label_name[]': ['cartoon', 'tv'],
'not[label_name][]': ['live action', 'drama'],
+ release_tag: ['v3', 'v4'],
+ 'not[release_tag]': ['v20', 'v30'],
+ 'type[]': ['issue', 'feature'],
+ 'not[type][]': ['bug', 'incident'],
my_reaction_emoji: 'thumbsup',
- confidential: 'no',
- iteration_id: '4',
- 'not[iteration_id]': '20',
+ 'not[my_reaction_emoji]': 'thumbsdown',
+ confidential: 'yes',
+ iteration_id: ['4', '12'],
+ 'not[iteration_id]': ['20', '42'],
epic_id: '12',
'not[epic_id]': '34',
weight: '1',
@@ -214,7 +251,8 @@ export const urlParams = {
export const urlParamsWithSpecialValues = {
assignee_id: '123',
'assignee_username[]': 'bart',
- 'type[]': ['issue', 'incident'],
+ 'label_name[]': 'None',
+ release_tag: 'None',
my_reaction_emoji: 'None',
iteration_id: 'Current',
milestone_title: 'Upcoming',
@@ -224,6 +262,7 @@ export const urlParamsWithSpecialValues = {
export const project1 = {
id: 'gid://gitlab/Group/26',
+ issuesEnabled: true,
name: 'Super Mario Project',
nameWithNamespace: 'Mushroom Kingdom / Super Mario Project',
webUrl: 'https://127.0.0.1:3000/mushroom-kingdom/super-mario-project',
@@ -231,16 +270,25 @@ export const project1 = {
export const project2 = {
id: 'gid://gitlab/Group/59',
+ issuesEnabled: false,
name: 'Mario Kart Project',
nameWithNamespace: 'Mushroom Kingdom / Mario Kart Project',
webUrl: 'https://127.0.0.1:3000/mushroom-kingdom/mario-kart-project',
};
+export const project3 = {
+ id: 'gid://gitlab/Group/103',
+ issuesEnabled: true,
+ name: 'Mario Party Project',
+ nameWithNamespace: 'Mushroom Kingdom / Mario Party Project',
+ webUrl: 'https://127.0.0.1:3000/mushroom-kingdom/mario-party-project',
+};
+
export const searchProjectsQueryResponse = {
data: {
group: {
projects: {
- nodes: [project1, project2],
+ nodes: [project1, project2, project3],
},
},
},
diff --git a/spec/frontend/issues_list/utils_spec.js b/spec/frontend/issues_list/utils_spec.js
index 458776d9ec5..8e1d70db92d 100644
--- a/spec/frontend/issues_list/utils_spec.js
+++ b/spec/frontend/issues_list/utils_spec.js
@@ -58,10 +58,10 @@ describe('getDueDateValue', () => {
describe('getSortOptions', () => {
describe.each`
hasIssueWeightsFeature | hasBlockedIssuesFeature | length | containsWeight | containsBlocking
- ${false} | ${false} | ${8} | ${false} | ${false}
- ${true} | ${false} | ${9} | ${true} | ${false}
- ${false} | ${true} | ${9} | ${false} | ${true}
- ${true} | ${true} | ${10} | ${true} | ${true}
+ ${false} | ${false} | ${9} | ${false} | ${false}
+ ${true} | ${false} | ${10} | ${true} | ${false}
+ ${false} | ${true} | ${10} | ${false} | ${true}
+ ${true} | ${true} | ${11} | ${true} | ${true}
`(
'when hasIssueWeightsFeature=$hasIssueWeightsFeature and hasBlockedIssuesFeature=$hasBlockedIssuesFeature',
({
diff --git a/spec/frontend/jira_connect/subscriptions/components/add_namespace_button_spec.js b/spec/frontend/jira_connect/subscriptions/components/add_namespace_button_spec.js
new file mode 100644
index 00000000000..5ec1b7b7932
--- /dev/null
+++ b/spec/frontend/jira_connect/subscriptions/components/add_namespace_button_spec.js
@@ -0,0 +1,44 @@
+import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import AddNamespaceButton from '~/jira_connect/subscriptions/components/add_namespace_button.vue';
+import AddNamespaceModal from '~/jira_connect/subscriptions/components/add_namespace_modal/add_namespace_modal.vue';
+import { ADD_NAMESPACE_MODAL_ID } from '~/jira_connect/subscriptions/constants';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+
+describe('AddNamespaceButton', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(AddNamespaceButton, {
+ directives: {
+ glModal: createMockDirective(),
+ },
+ });
+ };
+
+ const findButton = () => wrapper.findComponent(GlButton);
+ const findModal = () => wrapper.findComponent(AddNamespaceModal);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('displays a button', () => {
+ expect(findButton().exists()).toBe(true);
+ });
+
+ it('contains a modal', () => {
+ expect(findModal().exists()).toBe(true);
+ });
+
+ it('button is bound to the modal', () => {
+ const { value } = getBinding(findButton().element, 'gl-modal');
+
+ expect(value).toBeTruthy();
+ expect(value).toBe(ADD_NAMESPACE_MODAL_ID);
+ });
+});
diff --git a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/add_namespace_modal_spec.js b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/add_namespace_modal_spec.js
new file mode 100644
index 00000000000..d80381107f2
--- /dev/null
+++ b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/add_namespace_modal_spec.js
@@ -0,0 +1,36 @@
+import { shallowMount } from '@vue/test-utils';
+import AddNamespaceModal from '~/jira_connect/subscriptions/components/add_namespace_modal/add_namespace_modal.vue';
+import GroupsList from '~/jira_connect/subscriptions/components/add_namespace_modal/groups_list.vue';
+import { ADD_NAMESPACE_MODAL_ID } from '~/jira_connect/subscriptions/constants';
+
+describe('AddNamespaceModal', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(AddNamespaceModal);
+ };
+
+ const findModal = () => wrapper.findComponent(AddNamespaceModal);
+ const findGroupsList = () => wrapper.findComponent(GroupsList);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('displays modal with correct props', () => {
+ const modal = findModal();
+ expect(modal.exists()).toBe(true);
+ expect(modal.attributes()).toMatchObject({
+ modalid: ADD_NAMESPACE_MODAL_ID,
+ title: AddNamespaceModal.modal.title,
+ });
+ });
+
+ it('displays GroupList', () => {
+ expect(findGroupsList().exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/jira_connect/subscriptions/components/groups_list_item_spec.js b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js
index b69435df83a..15e9a740c83 100644
--- a/spec/frontend/jira_connect/subscriptions/components/groups_list_item_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js
@@ -4,9 +4,9 @@ import waitForPromises from 'helpers/wait_for_promises';
import * as JiraConnectApi from '~/jira_connect/subscriptions/api';
import GroupItemName from '~/jira_connect/subscriptions/components/group_item_name.vue';
-import GroupsListItem from '~/jira_connect/subscriptions/components/groups_list_item.vue';
+import GroupsListItem from '~/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item.vue';
import { persistAlert, reloadPage } from '~/jira_connect/subscriptions/utils';
-import { mockGroup1 } from '../mock_data';
+import { mockGroup1 } from '../../mock_data';
jest.mock('~/jira_connect/subscriptions/utils');
diff --git a/spec/frontend/jira_connect/subscriptions/components/groups_list_spec.js b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js
index d3a9a3bfd41..04aba8bda23 100644
--- a/spec/frontend/jira_connect/subscriptions/components/groups_list_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js
@@ -3,10 +3,10 @@ import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { fetchGroups } from '~/jira_connect/subscriptions/api';
-import GroupsList from '~/jira_connect/subscriptions/components/groups_list.vue';
-import GroupsListItem from '~/jira_connect/subscriptions/components/groups_list_item.vue';
+import GroupsList from '~/jira_connect/subscriptions/components/add_namespace_modal/groups_list.vue';
+import GroupsListItem from '~/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item.vue';
import { DEFAULT_GROUPS_PER_PAGE } from '~/jira_connect/subscriptions/constants';
-import { mockGroup1, mockGroup2 } from '../mock_data';
+import { mockGroup1, mockGroup2 } from '../../mock_data';
const createMockGroup = (groupId) => {
return {
diff --git a/spec/frontend/jira_connect/subscriptions/components/app_spec.js b/spec/frontend/jira_connect/subscriptions/components/app_spec.js
index 8915a7697a5..8e464968453 100644
--- a/spec/frontend/jira_connect/subscriptions/components/app_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/app_spec.js
@@ -1,14 +1,17 @@
-import { GlAlert, GlButton, GlModal, GlLink } from '@gitlab/ui';
+import { GlAlert, GlLink, GlEmptyState } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import JiraConnectApp from '~/jira_connect/subscriptions/components/app.vue';
+import AddNamespaceButton from '~/jira_connect/subscriptions/components/add_namespace_button.vue';
+import SignInButton from '~/jira_connect/subscriptions/components/sign_in_button.vue';
+import SubscriptionsList from '~/jira_connect/subscriptions/components/subscriptions_list.vue';
import createStore from '~/jira_connect/subscriptions/store';
import { SET_ALERT } from '~/jira_connect/subscriptions/store/mutation_types';
import { __ } from '~/locale';
+import { mockSubscription } from '../mock_data';
jest.mock('~/jira_connect/subscriptions/utils', () => ({
retrieveAlert: jest.fn().mockReturnValue({ message: 'error message' }),
- getLocation: jest.fn(),
}));
describe('JiraConnectApp', () => {
@@ -17,8 +20,10 @@ describe('JiraConnectApp', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
const findAlertLink = () => findAlert().findComponent(GlLink);
- const findGlButton = () => wrapper.findComponent(GlButton);
- const findGlModal = () => wrapper.findComponent(GlModal);
+ const findSignInButton = () => wrapper.findComponent(SignInButton);
+ const findAddNamespaceButton = () => wrapper.findComponent(AddNamespaceButton);
+ const findSubscriptionsList = () => wrapper.findComponent(SubscriptionsList);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const createComponent = ({ provide, mountFn = shallowMount } = {}) => {
store = createStore();
@@ -34,96 +39,115 @@ describe('JiraConnectApp', () => {
});
describe('template', () => {
- describe('when user is not logged in', () => {
- beforeEach(() => {
- createComponent({
- provide: {
- usersPath: '/users',
- },
+ describe.each`
+ scenario | usersPath | subscriptions | expectSignInButton | expectEmptyState | expectNamespaceButton | expectSubscriptionsList
+ ${'user is not signed in with subscriptions'} | ${'/users'} | ${[mockSubscription]} | ${true} | ${false} | ${false} | ${true}
+ ${'user is not signed in without subscriptions'} | ${'/users'} | ${undefined} | ${true} | ${false} | ${false} | ${false}
+ ${'user is signed in with subscriptions'} | ${undefined} | ${[mockSubscription]} | ${false} | ${false} | ${true} | ${true}
+ ${'user is signed in without subscriptions'} | ${undefined} | ${undefined} | ${false} | ${true} | ${false} | ${false}
+ `(
+ 'when $scenario',
+ ({
+ usersPath,
+ expectSignInButton,
+ subscriptions,
+ expectEmptyState,
+ expectNamespaceButton,
+ expectSubscriptionsList,
+ }) => {
+ beforeEach(() => {
+ createComponent({
+ provide: {
+ usersPath,
+ subscriptions,
+ },
+ });
});
- });
- it('renders "Sign in" button', () => {
- expect(findGlButton().text()).toBe('Sign in to add namespaces');
- expect(findGlModal().exists()).toBe(false);
- });
- });
+ it(`${expectSignInButton ? 'renders' : 'does not render'} sign in button`, () => {
+ expect(findSignInButton().exists()).toBe(expectSignInButton);
+ });
- describe('when user is logged in', () => {
- beforeEach(() => {
- createComponent();
- });
+ it(`${expectEmptyState ? 'renders' : 'does not render'} empty state`, () => {
+ expect(findEmptyState().exists()).toBe(expectEmptyState);
+ });
- it('renders "Add" button and modal', () => {
- expect(findGlButton().text()).toBe('Add namespace');
- expect(findGlModal().exists()).toBe(true);
- });
- });
+ it(`${
+ expectNamespaceButton ? 'renders' : 'does not render'
+ } button to add namespace`, () => {
+ expect(findAddNamespaceButton().exists()).toBe(expectNamespaceButton);
+ });
+
+ it(`${expectSubscriptionsList ? 'renders' : 'does not render'} subscriptions list`, () => {
+ expect(findSubscriptionsList().exists()).toBe(expectSubscriptionsList);
+ });
+ },
+ );
+ });
- describe('alert', () => {
- it.each`
- message | variant | alertShouldRender
- ${'Test error'} | ${'danger'} | ${true}
- ${'Test notice'} | ${'info'} | ${true}
- ${''} | ${undefined} | ${false}
- ${undefined} | ${undefined} | ${false}
- `(
- 'renders correct alert when message is `$message` and variant is `$variant`',
- async ({ message, alertShouldRender, variant }) => {
- createComponent();
-
- store.commit(SET_ALERT, { message, variant });
- await wrapper.vm.$nextTick();
-
- const alert = findAlert();
-
- expect(alert.exists()).toBe(alertShouldRender);
- if (alertShouldRender) {
- expect(alert.isVisible()).toBe(alertShouldRender);
- expect(alert.html()).toContain(message);
- expect(alert.props('variant')).toBe(variant);
- expect(findAlertLink().exists()).toBe(false);
- }
- },
- );
-
- it('hides alert on @dismiss event', async () => {
+ describe('alert', () => {
+ it.each`
+ message | variant | alertShouldRender
+ ${'Test error'} | ${'danger'} | ${true}
+ ${'Test notice'} | ${'info'} | ${true}
+ ${''} | ${undefined} | ${false}
+ ${undefined} | ${undefined} | ${false}
+ `(
+ 'renders correct alert when message is `$message` and variant is `$variant`',
+ async ({ message, alertShouldRender, variant }) => {
createComponent();
- store.commit(SET_ALERT, { message: 'test message' });
+ store.commit(SET_ALERT, { message, variant });
await wrapper.vm.$nextTick();
- findAlert().vm.$emit('dismiss');
- await wrapper.vm.$nextTick();
+ const alert = findAlert();
- expect(findAlert().exists()).toBe(false);
- });
+ expect(alert.exists()).toBe(alertShouldRender);
+ if (alertShouldRender) {
+ expect(alert.isVisible()).toBe(alertShouldRender);
+ expect(alert.html()).toContain(message);
+ expect(alert.props('variant')).toBe(variant);
+ expect(findAlertLink().exists()).toBe(false);
+ }
+ },
+ );
- it('renders link when `linkUrl` is set', async () => {
- createComponent({ mountFn: mount });
+ it('hides alert on @dismiss event', async () => {
+ createComponent();
- store.commit(SET_ALERT, {
- message: __('test message %{linkStart}test link%{linkEnd}'),
- linkUrl: 'https://gitlab.com',
- });
- await wrapper.vm.$nextTick();
+ store.commit(SET_ALERT, { message: 'test message' });
+ await wrapper.vm.$nextTick();
+
+ findAlert().vm.$emit('dismiss');
+ await wrapper.vm.$nextTick();
+
+ expect(findAlert().exists()).toBe(false);
+ });
- const alertLink = findAlertLink();
+ it('renders link when `linkUrl` is set', async () => {
+ createComponent({ mountFn: mount });
- expect(alertLink.exists()).toBe(true);
- expect(alertLink.text()).toContain('test link');
- expect(alertLink.attributes('href')).toBe('https://gitlab.com');
+ store.commit(SET_ALERT, {
+ message: __('test message %{linkStart}test link%{linkEnd}'),
+ linkUrl: 'https://gitlab.com',
});
+ await wrapper.vm.$nextTick();
- describe('when alert is set in localStoage', () => {
- it('renders alert on mount', () => {
- createComponent();
+ const alertLink = findAlertLink();
- const alert = findAlert();
+ expect(alertLink.exists()).toBe(true);
+ expect(alertLink.text()).toContain('test link');
+ expect(alertLink.attributes('href')).toBe('https://gitlab.com');
+ });
- expect(alert.exists()).toBe(true);
- expect(alert.html()).toContain('error message');
- });
+ describe('when alert is set in localStoage', () => {
+ it('renders alert on mount', () => {
+ createComponent();
+
+ const alert = findAlert();
+
+ expect(alert.exists()).toBe(true);
+ expect(alert.html()).toContain('error message');
});
});
});
diff --git a/spec/frontend/jira_connect/subscriptions/components/sign_in_button_spec.js b/spec/frontend/jira_connect/subscriptions/components/sign_in_button_spec.js
new file mode 100644
index 00000000000..cb5ae877c47
--- /dev/null
+++ b/spec/frontend/jira_connect/subscriptions/components/sign_in_button_spec.js
@@ -0,0 +1,48 @@
+import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { getGitlabSignInURL } from '~/jira_connect/subscriptions/utils';
+import SignInButton from '~/jira_connect/subscriptions/components/sign_in_button.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+
+const MOCK_USERS_PATH = '/user';
+
+jest.mock('~/jira_connect/subscriptions/utils');
+
+describe('SignInButton', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(SignInButton, {
+ propsData: {
+ usersPath: MOCK_USERS_PATH,
+ },
+ });
+ };
+
+ const findButton = () => wrapper.findComponent(GlButton);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('displays a button', () => {
+ createComponent();
+
+ expect(findButton().exists()).toBe(true);
+ });
+
+ describe.each`
+ expectedHref
+ ${MOCK_USERS_PATH}
+ ${`${MOCK_USERS_PATH}?return_to=${encodeURIComponent('https://test.jira.com')}`}
+ `('when getGitlabSignInURL resolves with `$expectedHref`', ({ expectedHref }) => {
+ it(`sets button href to ${expectedHref}`, async () => {
+ getGitlabSignInURL.mockResolvedValue(expectedHref);
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findButton().attributes('href')).toBe(expectedHref);
+ });
+ });
+});
diff --git a/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js b/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js
index 32b43765843..4e4a2b58600 100644
--- a/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js
@@ -1,12 +1,15 @@
-import { GlButton, GlEmptyState, GlTable } from '@gitlab/ui';
-import { mount, shallowMount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import * as JiraConnectApi from '~/jira_connect/subscriptions/api';
+import GroupItemName from '~/jira_connect/subscriptions/components/group_item_name.vue';
+
import SubscriptionsList from '~/jira_connect/subscriptions/components/subscriptions_list.vue';
import createStore from '~/jira_connect/subscriptions/store';
import { SET_ALERT } from '~/jira_connect/subscriptions/store/mutation_types';
import { reloadPage } from '~/jira_connect/subscriptions/utils';
+import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { mockSubscription } from '../mock_data';
jest.mock('~/jira_connect/subscriptions/utils');
@@ -15,11 +18,13 @@ describe('SubscriptionsList', () => {
let wrapper;
let store;
- const createComponent = ({ mountFn = shallowMount, provide = {} } = {}) => {
+ const createComponent = () => {
store = createStore();
- wrapper = mountFn(SubscriptionsList, {
- provide,
+ wrapper = mount(SubscriptionsList, {
+ provide: {
+ subscriptions: [mockSubscription],
+ },
store,
});
};
@@ -28,28 +33,28 @@ describe('SubscriptionsList', () => {
wrapper.destroy();
});
- const findGlEmptyState = () => wrapper.findComponent(GlEmptyState);
- const findGlTable = () => wrapper.findComponent(GlTable);
- const findUnlinkButton = () => findGlTable().findComponent(GlButton);
+ const findUnlinkButton = () => wrapper.findComponent(GlButton);
const clickUnlinkButton = () => findUnlinkButton().trigger('click');
describe('template', () => {
- it('renders GlEmptyState when subscriptions is empty', () => {
+ beforeEach(() => {
createComponent();
+ });
+
+ it('renders "name" cell correctly', () => {
+ const groupItemNames = wrapper.findAllComponents(GroupItemName);
+ expect(groupItemNames.wrappers).toHaveLength(1);
- expect(findGlEmptyState().exists()).toBe(true);
- expect(findGlTable().exists()).toBe(false);
+ const item = groupItemNames.at(0);
+ expect(item.props('group')).toBe(mockSubscription.group);
});
- it('renders GlTable when subscriptions are present', () => {
- createComponent({
- provide: {
- subscriptions: [mockSubscription],
- },
- });
+ it('renders "created at" cell correctly', () => {
+ const timeAgoTooltips = wrapper.findAllComponents(TimeagoTooltip);
+ expect(timeAgoTooltips.wrappers).toHaveLength(1);
- expect(findGlEmptyState().exists()).toBe(false);
- expect(findGlTable().exists()).toBe(true);
+ const item = timeAgoTooltips.at(0);
+ expect(item.props('time')).toBe(mockSubscription.created_at);
});
});
@@ -57,12 +62,7 @@ describe('SubscriptionsList', () => {
let removeSubscriptionSpy;
beforeEach(() => {
- createComponent({
- mountFn: mount,
- provide: {
- subscriptions: [mockSubscription],
- },
- });
+ createComponent();
removeSubscriptionSpy = jest.spyOn(JiraConnectApi, 'removeSubscription').mockResolvedValue();
});
diff --git a/spec/frontend/jira_connect/subscriptions/index_spec.js b/spec/frontend/jira_connect/subscriptions/index_spec.js
index 786f3b4a7d3..b97918a198e 100644
--- a/spec/frontend/jira_connect/subscriptions/index_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/index_spec.js
@@ -1,24 +1,36 @@
import { initJiraConnect } from '~/jira_connect/subscriptions';
+import { getGitlabSignInURL } from '~/jira_connect/subscriptions/utils';
-jest.mock('~/jira_connect/subscriptions/utils', () => ({
- getLocation: jest.fn().mockResolvedValue('test/location'),
-}));
+jest.mock('~/jira_connect/subscriptions/utils');
describe('initJiraConnect', () => {
- beforeEach(async () => {
+ const mockInitialHref = 'https://gitlab.com';
+
+ beforeEach(() => {
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 class="js-jira-connect-sign-in" href="${mockInitialHref}">Sign In</a>
+ <a class="js-jira-connect-sign-in" href="${mockInitialHref}">Another Sign In</a>
`);
-
- await initJiraConnect();
});
+ const assertSignInLinks = (expectedLink) => {
+ Array.from(document.querySelectorAll('.js-jira-connect-sign-in')).forEach((el) => {
+ expect(el.getAttribute('href')).toBe(expectedLink);
+ });
+ };
+
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');
- });
+ it('are updated on initialization', async () => {
+ const mockSignInLink = `https://gitlab.com?return_to=${encodeURIComponent('/test/location')}`;
+ getGitlabSignInURL.mockResolvedValue(mockSignInLink);
+
+ // assert the initial state
+ assertSignInLinks(mockInitialHref);
+
+ await initJiraConnect();
+
+ // assert the update has occurred
+ assertSignInLinks(mockSignInLink);
});
});
});
diff --git a/spec/frontend/jira_connect/subscriptions/utils_spec.js b/spec/frontend/jira_connect/subscriptions/utils_spec.js
index 2dd95de1b8c..762d9eb3443 100644
--- a/spec/frontend/jira_connect/subscriptions/utils_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/utils_spec.js
@@ -8,6 +8,7 @@ import {
getLocation,
reloadPage,
sizeToParent,
+ getGitlabSignInURL,
} from '~/jira_connect/subscriptions/utils';
describe('JiraConnect utils', () => {
@@ -137,4 +138,25 @@ describe('JiraConnect utils', () => {
});
});
});
+
+ describe('getGitlabSignInURL', () => {
+ const mockSignInURL = 'https://gitlab.com/sign_in';
+
+ it.each`
+ returnTo | expectResult
+ ${undefined} | ${mockSignInURL}
+ ${''} | ${mockSignInURL}
+ ${'/test/location'} | ${`${mockSignInURL}?return_to=${encodeURIComponent('/test/location')}`}
+ `(
+ 'returns `$expectResult` when `AP.getLocation` resolves to `$returnTo`',
+ async ({ returnTo, expectResult }) => {
+ global.AP = {
+ getLocation: jest.fn().mockImplementation((cb) => cb(returnTo)),
+ };
+
+ const url = await getGitlabSignInURL(mockSignInURL);
+ expect(url).toBe(expectResult);
+ },
+ );
+ });
});
diff --git a/spec/frontend/jobs/components/manual_variables_form_spec.js b/spec/frontend/jobs/components/manual_variables_form_spec.js
index 7e42ee957d3..a5278af8e33 100644
--- a/spec/frontend/jobs/components/manual_variables_form_spec.js
+++ b/spec/frontend/jobs/components/manual_variables_form_spec.js
@@ -1,9 +1,9 @@
import { GlSprintf, GlLink } from '@gitlab/ui';
-import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import { createLocalVue, mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import Form from '~/jobs/components/manual_variables_form.vue';
+import ManualVariablesForm from '~/jobs/components/manual_variables_form.vue';
const localVue = createLocalVue();
@@ -21,7 +21,7 @@ describe('Manual Variables Form', () => {
},
};
- const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => {
+ const createComponent = (props = {}) => {
store = new Vuex.Store({
actions: {
triggerManualJob: jest.fn(),
@@ -29,7 +29,7 @@ describe('Manual Variables Form', () => {
});
wrapper = extendedWrapper(
- mountFn(localVue.extend(Form), {
+ mount(localVue.extend(ManualVariablesForm), {
propsData: { ...requiredProps, ...props },
localVue,
store,
@@ -40,88 +40,120 @@ describe('Manual Variables Form', () => {
);
};
- const findInputKey = () => wrapper.findComponent({ ref: 'inputKey' });
- const findInputValue = () => wrapper.findComponent({ ref: 'inputSecretValue' });
const findHelpText = () => wrapper.findComponent(GlSprintf);
const findHelpLink = () => wrapper.findComponent(GlLink);
const findTriggerBtn = () => wrapper.findByTestId('trigger-manual-job-btn');
const findDeleteVarBtn = () => wrapper.findByTestId('delete-variable-btn');
+ const findAllDeleteVarBtns = () => wrapper.findAllByTestId('delete-variable-btn');
+ const findDeleteVarBtnPlaceholder = () => wrapper.findByTestId('delete-variable-btn-placeholder');
const findCiVariableKey = () => wrapper.findByTestId('ci-variable-key');
+ const findAllCiVariableKeys = () => wrapper.findAllByTestId('ci-variable-key');
const findCiVariableValue = () => wrapper.findByTestId('ci-variable-value');
const findAllVariables = () => wrapper.findAllByTestId('ci-variable-row');
+ const setCiVariableKey = () => {
+ findCiVariableKey().setValue('new key');
+ findCiVariableKey().vm.$emit('change');
+ nextTick();
+ };
+
+ const setCiVariableKeyByPosition = (position, value) => {
+ findAllCiVariableKeys().at(position).setValue(value);
+ findAllCiVariableKeys().at(position).vm.$emit('change');
+ nextTick();
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
afterEach(() => {
wrapper.destroy();
});
- describe('shallowMount', () => {
- beforeEach(() => {
- createComponent();
- });
+ it('creates a new variable when user enters a new key value', async () => {
+ expect(findAllVariables()).toHaveLength(1);
- it('renders empty form with correct placeholders', () => {
- expect(findInputKey().attributes('placeholder')).toBe('Input variable key');
- expect(findInputValue().attributes('placeholder')).toBe('Input variable value');
- });
+ await setCiVariableKey();
- it('renders help text with provided link', () => {
- expect(findHelpText().exists()).toBe(true);
- expect(findHelpLink().attributes('href')).toBe(
- '/help/ci/variables/index#add-a-cicd-variable-to-a-project',
- );
- });
+ expect(findAllVariables()).toHaveLength(2);
+ });
- describe('when adding a new variable', () => {
- it('creates a new variable when user types a new key and resets the form', async () => {
- await findInputKey().setValue('new key');
+ it('does not create extra empty variables', async () => {
+ expect(findAllVariables()).toHaveLength(1);
- expect(findAllVariables()).toHaveLength(1);
- expect(findCiVariableKey().element.value).toBe('new key');
- expect(findInputKey().attributes('value')).toBe(undefined);
- });
+ await setCiVariableKey();
- it('creates a new variable when user types a new value and resets the form', async () => {
- await findInputValue().setValue('new value');
+ expect(findAllVariables()).toHaveLength(2);
- expect(findAllVariables()).toHaveLength(1);
- expect(findCiVariableValue().element.value).toBe('new value');
- expect(findInputValue().attributes('value')).toBe(undefined);
- });
- });
+ await setCiVariableKey();
+
+ expect(findAllVariables()).toHaveLength(2);
});
- describe('mount', () => {
- beforeEach(() => {
- createComponent({ mountFn: mount });
- });
+ it('removes the correct variable row', async () => {
+ const variableKeyNameOne = 'key-one';
+ const variableKeyNameThree = 'key-three';
- describe('when deleting a variable', () => {
- it('removes the variable row', async () => {
- await wrapper.setData({
- variables: [
- {
- key: 'new key',
- secret_value: 'value',
- id: '1',
- },
- ],
- });
+ await setCiVariableKeyByPosition(0, variableKeyNameOne);
- findDeleteVarBtn().trigger('click');
+ await setCiVariableKeyByPosition(1, 'key-two');
- await wrapper.vm.$nextTick();
+ await setCiVariableKeyByPosition(2, variableKeyNameThree);
- expect(findAllVariables()).toHaveLength(0);
- });
- });
+ expect(findAllVariables()).toHaveLength(4);
- it('trigger button is disabled after trigger action', async () => {
- expect(findTriggerBtn().props('disabled')).toBe(false);
+ await findAllDeleteVarBtns().at(1).trigger('click');
- await findTriggerBtn().trigger('click');
+ expect(findAllVariables()).toHaveLength(3);
- expect(findTriggerBtn().props('disabled')).toBe(true);
- });
+ expect(findAllCiVariableKeys().at(0).element.value).toBe(variableKeyNameOne);
+ expect(findAllCiVariableKeys().at(1).element.value).toBe(variableKeyNameThree);
+ expect(findAllCiVariableKeys().at(2).element.value).toBe('');
+ });
+
+ it('trigger button is disabled after trigger action', async () => {
+ expect(findTriggerBtn().props('disabled')).toBe(false);
+
+ await findTriggerBtn().trigger('click');
+
+ expect(findTriggerBtn().props('disabled')).toBe(true);
+ });
+
+ it('delete variable button should only show when there is more than one variable', async () => {
+ expect(findDeleteVarBtn().exists()).toBe(false);
+
+ await setCiVariableKey();
+
+ expect(findDeleteVarBtn().exists()).toBe(true);
+ });
+
+ it('delete variable button placeholder should only exist when a user cannot remove', async () => {
+ expect(findDeleteVarBtnPlaceholder().exists()).toBe(true);
+ });
+
+ it('renders help text with provided link', () => {
+ expect(findHelpText().exists()).toBe(true);
+ expect(findHelpLink().attributes('href')).toBe(
+ '/help/ci/variables/index#add-a-cicd-variable-to-a-project',
+ );
+ });
+
+ it('passes variables in correct format', async () => {
+ jest.spyOn(store, 'dispatch');
+
+ await setCiVariableKey();
+
+ await findCiVariableValue().setValue('new value');
+
+ await findTriggerBtn().trigger('click');
+
+ expect(store.dispatch).toHaveBeenCalledWith('triggerManualJob', [
+ {
+ key: 'new key',
+ secret_value: 'new value',
+ },
+ ]);
});
});
diff --git a/spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js b/spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js
index 852106db44e..7b604724977 100644
--- a/spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js
+++ b/spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js
@@ -47,107 +47,95 @@ describe('getSuppressNetworkErrorsDuringNavigationLink', () => {
subscription = link.request(mockOperation).subscribe(observer);
};
- describe('when disabled', () => {
- it('returns null', () => {
- expect(getSuppressNetworkErrorsDuringNavigationLink()).toBe(null);
- });
+ it('returns an ApolloLink', () => {
+ expect(getSuppressNetworkErrorsDuringNavigationLink()).toEqual(expect.any(ApolloLink));
});
- describe('when enabled', () => {
- beforeEach(() => {
- window.gon = { features: { suppressApolloErrorsDuringNavigation: true } };
- });
-
- it('returns an ApolloLink', () => {
- expect(getSuppressNetworkErrorsDuringNavigationLink()).toEqual(expect.any(ApolloLink));
- });
-
- describe('suppression case', () => {
- describe('when navigating away', () => {
- beforeEach(() => {
- isNavigatingAway.mockReturnValue(true);
- });
-
- describe('given a network error', () => {
- it('does not forward the error', async () => {
- const spy = jest.fn();
+ describe('suppression case', () => {
+ describe('when navigating away', () => {
+ beforeEach(() => {
+ isNavigatingAway.mockReturnValue(true);
+ });
- createSubscription(makeMockNetworkErrorLink(), {
- next: spy,
- error: spy,
- complete: spy,
- });
+ describe('given a network error', () => {
+ it('does not forward the error', async () => {
+ const spy = jest.fn();
- // It's hard to test for something _not_ happening. The best we can
- // do is wait a bit to make sure nothing happens.
- await waitForPromises();
- expect(spy).not.toHaveBeenCalled();
+ createSubscription(makeMockNetworkErrorLink(), {
+ next: spy,
+ error: spy,
+ complete: spy,
});
+
+ // It's hard to test for something _not_ happening. The best we can
+ // do is wait a bit to make sure nothing happens.
+ await waitForPromises();
+ expect(spy).not.toHaveBeenCalled();
});
});
});
+ });
- describe('non-suppression cases', () => {
- describe('when not navigating away', () => {
- beforeEach(() => {
- isNavigatingAway.mockReturnValue(false);
- });
+ describe('non-suppression cases', () => {
+ describe('when not navigating away', () => {
+ beforeEach(() => {
+ isNavigatingAway.mockReturnValue(false);
+ });
- it('forwards successful requests', (done) => {
- createSubscription(makeMockSuccessLink(), {
- next({ data }) {
- expect(data).toEqual({ foo: { id: 1 } });
- },
- error: () => done.fail('Should not happen'),
- complete: () => done(),
- });
+ it('forwards successful requests', (done) => {
+ createSubscription(makeMockSuccessLink(), {
+ next({ data }) {
+ expect(data).toEqual({ foo: { id: 1 } });
+ },
+ error: () => done.fail('Should not happen'),
+ complete: () => done(),
});
+ });
- it('forwards GraphQL errors', (done) => {
- createSubscription(makeMockGraphQLErrorLink(), {
- next({ errors }) {
- expect(errors).toEqual([{ message: 'foo' }]);
- },
- error: () => done.fail('Should not happen'),
- complete: () => done(),
- });
+ it('forwards GraphQL errors', (done) => {
+ createSubscription(makeMockGraphQLErrorLink(), {
+ next({ errors }) {
+ expect(errors).toEqual([{ message: 'foo' }]);
+ },
+ error: () => done.fail('Should not happen'),
+ complete: () => done(),
});
+ });
- it('forwards network errors', (done) => {
- createSubscription(makeMockNetworkErrorLink(), {
- next: () => done.fail('Should not happen'),
- error: (error) => {
- expect(error.message).toBe('NetworkError');
- done();
- },
- complete: () => done.fail('Should not happen'),
- });
+ it('forwards network errors', (done) => {
+ createSubscription(makeMockNetworkErrorLink(), {
+ next: () => done.fail('Should not happen'),
+ error: (error) => {
+ expect(error.message).toBe('NetworkError');
+ done();
+ },
+ complete: () => done.fail('Should not happen'),
});
});
+ });
- describe('when navigating away', () => {
- beforeEach(() => {
- isNavigatingAway.mockReturnValue(true);
- });
+ describe('when navigating away', () => {
+ beforeEach(() => {
+ isNavigatingAway.mockReturnValue(true);
+ });
- it('forwards successful requests', (done) => {
- createSubscription(makeMockSuccessLink(), {
- next({ data }) {
- expect(data).toEqual({ foo: { id: 1 } });
- },
- error: () => done.fail('Should not happen'),
- complete: () => done(),
- });
+ it('forwards successful requests', (done) => {
+ createSubscription(makeMockSuccessLink(), {
+ next({ data }) {
+ expect(data).toEqual({ foo: { id: 1 } });
+ },
+ error: () => done.fail('Should not happen'),
+ complete: () => done(),
});
+ });
- it('forwards GraphQL errors', (done) => {
- createSubscription(makeMockGraphQLErrorLink(), {
- next({ errors }) {
- expect(errors).toEqual([{ message: 'foo' }]);
- },
- error: () => done.fail('Should not happen'),
- complete: () => done(),
- });
+ it('forwards GraphQL errors', (done) => {
+ createSubscription(makeMockGraphQLErrorLink(), {
+ next({ errors }) {
+ expect(errors).toEqual([{ message: 'foo' }]);
+ },
+ error: () => done.fail('Should not happen'),
+ complete: () => done(),
});
});
});
diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js
index f5a74ee7f09..de1be5bc337 100644
--- a/spec/frontend/lib/utils/common_utils_spec.js
+++ b/spec/frontend/lib/utils/common_utils_spec.js
@@ -279,6 +279,14 @@ describe('common_utils', () => {
top: elementTopWithContext,
});
});
+
+ it('passes through behaviour', () => {
+ commonUtils.scrollToElementWithContext(`#${id}`, { behavior: 'smooth' });
+ expect(window.scrollTo).toHaveBeenCalledWith({
+ behavior: 'smooth',
+ top: elementTopWithContext,
+ });
+ });
});
});
@@ -1000,6 +1008,21 @@ describe('common_utils', () => {
});
});
+ describe('scopedLabelKey', () => {
+ it.each`
+ label | expectedLabelKey
+ ${undefined} | ${''}
+ ${''} | ${''}
+ ${'title'} | ${'title'}
+ ${'scoped::value'} | ${'scoped'}
+ ${'scoped::label::value'} | ${'scoped::label'}
+ ${'scoped::label-some::value'} | ${'scoped::label-some'}
+ ${'scoped::label::some::value'} | ${'scoped::label::some'}
+ `('returns "$expectedLabelKey" when label is "$label"', ({ label, expectedLabelKey }) => {
+ expect(commonUtils.scopedLabelKey({ title: label })).toBe(expectedLabelKey);
+ });
+ });
+
describe('getDashPath', () => {
it('returns the path following /-/', () => {
expect(commonUtils.getDashPath('/some/-/url-with-dashes-/')).toEqual('url-with-dashes-/');
diff --git a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js
new file mode 100644
index 00000000000..d19f9352bbc
--- /dev/null
+++ b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js
@@ -0,0 +1,59 @@
+import { GlModal } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import ConfirmModal from '~/lib/utils/confirm_via_gl_modal/confirm_modal.vue';
+
+describe('Confirm Modal', () => {
+ let wrapper;
+ let modal;
+
+ const createComponent = ({ primaryText, primaryVariant } = {}) => {
+ wrapper = mount(ConfirmModal, {
+ propsData: {
+ primaryText,
+ primaryVariant,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findGlModal = () => wrapper.findComponent(GlModal);
+
+ describe('Modal events', () => {
+ beforeEach(() => {
+ createComponent();
+ modal = findGlModal();
+ });
+
+ it('should emit `confirmed` event on `primary` modal event', () => {
+ findGlModal().vm.$emit('primary');
+ expect(wrapper.emitted('confirmed')).toBeTruthy();
+ });
+
+ it('should emit closed` event on `hidden` modal event', () => {
+ modal.vm.$emit('hidden');
+ expect(wrapper.emitted('closed')).toBeTruthy();
+ });
+ });
+
+ describe('Custom properties', () => {
+ it('should pass correct custom primary text & button variant to the modal when provided', () => {
+ const primaryText = "Let's do it!";
+ const primaryVariant = 'danger';
+
+ createComponent({ primaryText, primaryVariant });
+ const customProps = findGlModal().props('actionPrimary');
+ expect(customProps.text).toBe(primaryText);
+ expect(customProps.attributes.variant).toBe(primaryVariant);
+ });
+
+ it('should pass default primary text & button variant to the modal if no custom values provided', () => {
+ createComponent();
+ const customProps = findGlModal().props('actionPrimary');
+ expect(customProps.text).toBe('OK');
+ expect(customProps.attributes.variant).toBe('confirm');
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/datetime_utility_spec.js b/spec/frontend/lib/utils/datetime_utility_spec.js
index f6ad41d5478..7a64b654baa 100644
--- a/spec/frontend/lib/utils/datetime_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime_utility_spec.js
@@ -185,15 +185,15 @@ describe('dateInWords', () => {
const date = new Date('07/01/2016');
it('should return date in words', () => {
- expect(datetimeUtility.dateInWords(date)).toEqual(s__('July 1, 2016'));
+ expect(datetimeUtility.dateInWords(date)).toEqual(__('July 1, 2016'));
});
it('should return abbreviated month name', () => {
- expect(datetimeUtility.dateInWords(date, true)).toEqual(s__('Jul 1, 2016'));
+ expect(datetimeUtility.dateInWords(date, true)).toEqual(__('Jul 1, 2016'));
});
it('should return date in words without year', () => {
- expect(datetimeUtility.dateInWords(date, true, true)).toEqual(s__('Jul 1'));
+ expect(datetimeUtility.dateInWords(date, true, true)).toEqual(__('Jul 1'));
});
});
@@ -201,11 +201,11 @@ describe('monthInWords', () => {
const date = new Date('2017-01-20');
it('returns month name from provided date', () => {
- expect(datetimeUtility.monthInWords(date)).toBe(s__('January'));
+ expect(datetimeUtility.monthInWords(date)).toBe(__('January'));
});
it('returns abbreviated month name from provided date', () => {
- expect(datetimeUtility.monthInWords(date, true)).toBe(s__('Jan'));
+ expect(datetimeUtility.monthInWords(date, true)).toBe(__('Jan'));
});
});
diff --git a/spec/frontend/lib/utils/file_upload_spec.js b/spec/frontend/lib/utils/file_upload_spec.js
index 1dff5d4f925..ff11107ea60 100644
--- a/spec/frontend/lib/utils/file_upload_spec.js
+++ b/spec/frontend/lib/utils/file_upload_spec.js
@@ -1,4 +1,4 @@
-import fileUpload, { getFilename } from '~/lib/utils/file_upload';
+import fileUpload, { getFilename, validateImageName } from '~/lib/utils/file_upload';
describe('File upload', () => {
beforeEach(() => {
@@ -64,13 +64,23 @@ describe('File upload', () => {
});
describe('getFilename', () => {
- it('returns first value correctly', () => {
- const event = {
- clipboardData: {
- getData: () => 'test.png\rtest.txt',
- },
- };
-
- expect(getFilename(event)).toBe('test.png');
+ it('returns file name', () => {
+ const file = new File([], 'test.jpg');
+
+ expect(getFilename(file)).toBe('test.jpg');
+ });
+});
+
+describe('file name validator', () => {
+ it('validate file name', () => {
+ const file = new File([], 'test.jpg');
+
+ expect(validateImageName(file)).toBe('test.jpg');
+ });
+
+ it('illegal file name should be rename to image.png', () => {
+ const file = new File([], 'test<.png');
+
+ expect(validateImageName(file)).toBe('image.png');
});
});
diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js
index acbf1a975b8..ab81ec47b64 100644
--- a/spec/frontend/lib/utils/text_markdown_spec.js
+++ b/spec/frontend/lib/utils/text_markdown_spec.js
@@ -100,11 +100,11 @@ describe('init markdown', () => {
text: textArea.value,
tag: '```suggestion:-0+0\n{text}\n```',
blockTag: true,
- selected: '# Does not parse the %br currently.',
+ selected: '# Does not %br parse the %br currently.',
wrap: false,
});
- expect(textArea.value).toContain('# Does not parse the \\n currently.');
+ expect(textArea.value).toContain('# Does not \\n parse the \\n currently.');
});
it('inserts the tag on the same line if the current line only contains spaces', () => {
diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index 36e1a453ef4..c6edba19c56 100644
--- a/spec/frontend/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -1060,4 +1060,12 @@ describe('URL utility', () => {
},
);
});
+
+ describe('defaultPromoUrl', () => {
+ it('Gitlab about page url', () => {
+ const url = 'https://about.gitlab.com';
+
+ expect(urlUtils.PROMO_URL).toBe(url);
+ });
+ });
});
diff --git a/spec/frontend/members/mock_data.js b/spec/frontend/members/mock_data.js
index f42ee295511..218db0b587a 100644
--- a/spec/frontend/members/mock_data.js
+++ b/spec/frontend/members/mock_data.js
@@ -39,7 +39,7 @@ export const member = {
Developer: 30,
Maintainer: 40,
Owner: 50,
- 'Minimal Access': 5,
+ 'Minimal access': 5,
},
};
diff --git a/spec/frontend/monitoring/__snapshots__/alert_widget_spec.js.snap b/spec/frontend/monitoring/__snapshots__/alert_widget_spec.js.snap
deleted file mode 100644
index 2a8ce1d3f30..00000000000
--- a/spec/frontend/monitoring/__snapshots__/alert_widget_spec.js.snap
+++ /dev/null
@@ -1,43 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`AlertWidget Alert firing displays a warning icon and matches snapshot 1`] = `
-<gl-badge-stub
- class="d-flex-center text-truncate"
- size="md"
- variant="danger"
->
- <gl-icon-stub
- class="flex-shrink-0"
- name="warning"
- size="16"
- />
-
- <span
- class="text-truncate gl-pl-2"
- >
- Firing:
- alert-label &gt; 42
-
- </span>
-</gl-badge-stub>
-`;
-
-exports[`AlertWidget Alert not firing displays a warning icon and matches snapshot 1`] = `
-<gl-badge-stub
- class="d-flex-center text-truncate"
- size="md"
- variant="neutral"
->
- <gl-icon-stub
- class="flex-shrink-0"
- name="warning"
- size="16"
- />
-
- <span
- class="text-truncate gl-pl-2"
- >
- alert-label &gt; 42
- </span>
-</gl-badge-stub>
-`;
diff --git a/spec/frontend/monitoring/alert_widget_spec.js b/spec/frontend/monitoring/alert_widget_spec.js
deleted file mode 100644
index 9bf9e8ad7cc..00000000000
--- a/spec/frontend/monitoring/alert_widget_spec.js
+++ /dev/null
@@ -1,423 +0,0 @@
-import { GlLoadingIcon, GlTooltip, GlSprintf, GlBadge } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
-import AlertWidget from '~/monitoring/components/alert_widget.vue';
-
-const mockReadAlert = jest.fn();
-const mockCreateAlert = jest.fn();
-const mockUpdateAlert = jest.fn();
-const mockDeleteAlert = jest.fn();
-
-jest.mock('~/flash');
-jest.mock(
- '~/monitoring/services/alerts_service',
- () =>
- function AlertsServiceMock() {
- return {
- readAlert: mockReadAlert,
- createAlert: mockCreateAlert,
- updateAlert: mockUpdateAlert,
- deleteAlert: mockDeleteAlert,
- };
- },
-);
-
-describe('AlertWidget', () => {
- let wrapper;
-
- const nonFiringAlertResult = [
- {
- values: [
- [0, 1],
- [1, 42],
- [2, 41],
- ],
- },
- ];
- const firingAlertResult = [
- {
- values: [
- [0, 42],
- [1, 43],
- [2, 44],
- ],
- },
- ];
- const metricId = '5';
- const alertPath = 'my/alert.json';
-
- const relevantQueries = [
- {
- metricId,
- label: 'alert-label',
- alert_path: alertPath,
- result: nonFiringAlertResult,
- },
- ];
-
- const firingRelevantQueries = [
- {
- metricId,
- label: 'alert-label',
- alert_path: alertPath,
- result: firingAlertResult,
- },
- ];
-
- const defaultProps = {
- alertsEndpoint: '',
- relevantQueries,
- alertsToManage: {},
- modalId: 'alert-modal-1',
- };
-
- const propsWithAlert = {
- relevantQueries,
- };
-
- const propsWithAlertData = {
- relevantQueries,
- alertsToManage: {
- [alertPath]: { operator: '>', threshold: 42, alert_path: alertPath, metricId },
- },
- };
-
- const createComponent = (propsData) => {
- wrapper = shallowMount(AlertWidget, {
- stubs: { GlTooltip, GlSprintf },
- propsData: {
- ...defaultProps,
- ...propsData,
- },
- });
- };
- const hasLoadingIcon = () => wrapper.find(GlLoadingIcon).exists();
- const findWidgetForm = () => wrapper.find({ ref: 'widgetForm' });
- const findAlertErrorMessage = () => wrapper.find({ ref: 'alertErrorMessage' });
- const findCurrentSettingsText = () =>
- wrapper.find({ ref: 'alertCurrentSetting' }).text().replace(/\s\s+/g, ' ');
- const findBadge = () => wrapper.find(GlBadge);
- const findTooltip = () => wrapper.find(GlTooltip);
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- it('displays a loading spinner and disables form when fetching alerts', () => {
- let resolveReadAlert;
- mockReadAlert.mockReturnValue(
- new Promise((resolve) => {
- resolveReadAlert = resolve;
- }),
- );
- createComponent(defaultProps);
- return wrapper.vm
- .$nextTick()
- .then(() => {
- expect(hasLoadingIcon()).toBe(true);
- expect(findWidgetForm().props('disabled')).toBe(true);
-
- resolveReadAlert({ operator: '==', threshold: 42 });
- })
- .then(() => waitForPromises())
- .then(() => {
- expect(hasLoadingIcon()).toBe(false);
- expect(findWidgetForm().props('disabled')).toBe(false);
- });
- });
-
- it('does not render loading spinner if showLoadingState is false', () => {
- let resolveReadAlert;
- mockReadAlert.mockReturnValue(
- new Promise((resolve) => {
- resolveReadAlert = resolve;
- }),
- );
- createComponent({
- ...defaultProps,
- showLoadingState: false,
- });
- return wrapper.vm
- .$nextTick()
- .then(() => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
-
- resolveReadAlert({ operator: '==', threshold: 42 });
- })
- .then(() => waitForPromises())
- .then(() => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
- });
- });
-
- it('displays an error message when fetch fails', () => {
- mockReadAlert.mockRejectedValue();
- createComponent(propsWithAlert);
- expect(hasLoadingIcon()).toBe(true);
-
- return waitForPromises().then(() => {
- expect(createFlash).toHaveBeenCalled();
- expect(hasLoadingIcon()).toBe(false);
- });
- });
-
- describe('Alert not firing', () => {
- it('displays a warning icon and matches snapshot', () => {
- mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
- createComponent(propsWithAlertData);
-
- return waitForPromises().then(() => {
- expect(findBadge().element).toMatchSnapshot();
- });
- });
-
- it('displays an alert summary when there is a single alert', () => {
- mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
- createComponent(propsWithAlertData);
- return waitForPromises().then(() => {
- expect(findCurrentSettingsText()).toEqual('alert-label > 42');
- });
- });
-
- it('displays a combined alert summary when there are multiple alerts', () => {
- mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
- const propsWithManyAlerts = {
- relevantQueries: [
- ...relevantQueries,
- ...[
- {
- metricId: '6',
- alert_path: 'my/alert2.json',
- label: 'alert-label2',
- result: [{ values: [] }],
- },
- ],
- ],
- alertsToManage: {
- 'my/alert.json': {
- operator: '>',
- threshold: 42,
- alert_path: alertPath,
- metricId,
- },
- 'my/alert2.json': {
- operator: '==',
- threshold: 900,
- alert_path: 'my/alert2.json',
- metricId: '6',
- },
- },
- };
- createComponent(propsWithManyAlerts);
- return waitForPromises().then(() => {
- expect(findCurrentSettingsText()).toContain('2 alerts applied');
- });
- });
- });
-
- describe('Alert firing', () => {
- it('displays a warning icon and matches snapshot', () => {
- mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
- propsWithAlertData.relevantQueries = firingRelevantQueries;
- createComponent(propsWithAlertData);
-
- return waitForPromises().then(() => {
- expect(findBadge().element).toMatchSnapshot();
- });
- });
-
- it('displays an alert summary when there is a single alert', () => {
- mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
- propsWithAlertData.relevantQueries = firingRelevantQueries;
- createComponent(propsWithAlertData);
- return waitForPromises().then(() => {
- expect(findCurrentSettingsText()).toEqual('Firing: alert-label > 42');
- });
- });
-
- it('displays a combined alert summary when there are multiple alerts', () => {
- mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
- const propsWithManyAlerts = {
- relevantQueries: [
- ...firingRelevantQueries,
- ...[
- {
- metricId: '6',
- alert_path: 'my/alert2.json',
- label: 'alert-label2',
- result: [{ values: [] }],
- },
- ],
- ],
- alertsToManage: {
- 'my/alert.json': {
- operator: '>',
- threshold: 42,
- alert_path: alertPath,
- metricId,
- },
- 'my/alert2.json': {
- operator: '==',
- threshold: 900,
- alert_path: 'my/alert2.json',
- metricId: '6',
- },
- },
- };
- createComponent(propsWithManyAlerts);
-
- return waitForPromises().then(() => {
- expect(findCurrentSettingsText()).toContain('2 alerts applied, 1 firing');
- });
- });
-
- it('should display tooltip with thresholds summary', () => {
- mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
- const propsWithManyAlerts = {
- relevantQueries: [
- ...firingRelevantQueries,
- ...[
- {
- metricId: '6',
- alert_path: 'my/alert2.json',
- label: 'alert-label2',
- result: [{ values: [] }],
- },
- ],
- ],
- alertsToManage: {
- 'my/alert.json': {
- operator: '>',
- threshold: 42,
- alert_path: alertPath,
- metricId,
- },
- 'my/alert2.json': {
- operator: '==',
- threshold: 900,
- alert_path: 'my/alert2.json',
- metricId: '6',
- },
- },
- };
- createComponent(propsWithManyAlerts);
-
- return waitForPromises().then(() => {
- expect(findTooltip().text().replace(/\s\s+/g, ' ')).toEqual('Firing: alert-label > 42');
- });
- });
- });
-
- it('creates an alert with an appropriate handler', () => {
- const alertParams = {
- operator: '<',
- threshold: 4,
- prometheus_metric_id: '5',
- };
- mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
- const fakeAlertPath = 'foo/bar';
- mockCreateAlert.mockResolvedValue({ alert_path: fakeAlertPath, ...alertParams });
- createComponent({
- alertsToManage: {
- [fakeAlertPath]: {
- alert_path: fakeAlertPath,
- operator: '<',
- threshold: 4,
- prometheus_metric_id: '5',
- metricId: '5',
- },
- },
- });
-
- findWidgetForm().vm.$emit('create', alertParams);
-
- expect(mockCreateAlert).toHaveBeenCalledWith(alertParams);
- });
-
- it('updates an alert with an appropriate handler', () => {
- const alertParams = { operator: '<', threshold: 4, alert_path: alertPath };
- const newAlertParams = { operator: '==', threshold: 12 };
- mockReadAlert.mockResolvedValue(alertParams);
- mockUpdateAlert.mockResolvedValue({ ...alertParams, ...newAlertParams });
- createComponent({
- ...propsWithAlertData,
- alertsToManage: {
- [alertPath]: {
- alert_path: alertPath,
- operator: '==',
- threshold: 12,
- metricId: '5',
- },
- },
- });
-
- findWidgetForm().vm.$emit('update', {
- alert: alertPath,
- ...newAlertParams,
- prometheus_metric_id: '5',
- });
-
- expect(mockUpdateAlert).toHaveBeenCalledWith(alertPath, newAlertParams);
- });
-
- it('deletes an alert with an appropriate handler', () => {
- const alertParams = { alert_path: alertPath, operator: '>', threshold: 42 };
- mockReadAlert.mockResolvedValue(alertParams);
- mockDeleteAlert.mockResolvedValue({});
- createComponent({
- ...propsWithAlert,
- alertsToManage: {
- [alertPath]: {
- alert_path: alertPath,
- operator: '>',
- threshold: 42,
- metricId: '5',
- },
- },
- });
-
- findWidgetForm().vm.$emit('delete', { alert: alertPath });
-
- return wrapper.vm.$nextTick().then(() => {
- expect(mockDeleteAlert).toHaveBeenCalledWith(alertPath);
- expect(findAlertErrorMessage().exists()).toBe(false);
- });
- });
-
- describe('when delete fails', () => {
- beforeEach(() => {
- const alertParams = { alert_path: alertPath, operator: '>', threshold: 42 };
- mockReadAlert.mockResolvedValue(alertParams);
- mockDeleteAlert.mockRejectedValue();
-
- createComponent({
- ...propsWithAlert,
- alertsToManage: {
- [alertPath]: {
- alert_path: alertPath,
- operator: '>',
- threshold: 42,
- metricId: '5',
- },
- },
- });
-
- findWidgetForm().vm.$emit('delete', { alert: alertPath });
- return wrapper.vm.$nextTick();
- });
-
- it('shows error message', () => {
- expect(findAlertErrorMessage().text()).toEqual('Error deleting alert');
- });
-
- it('dismisses error message on cancel', () => {
- findWidgetForm().vm.$emit('cancel');
-
- return wrapper.vm.$nextTick().then(() => {
- expect(findAlertErrorMessage().exists()).toBe(false);
- });
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
index 47b6c463377..aaa0a91ffe0 100644
--- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
+++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
@@ -8,8 +8,6 @@ exports[`Dashboard template matches the default snapshot 1`] = `
metricsdashboardbasepath="/monitoring/monitor-project/-/environments/1/metrics"
metricsendpoint="/monitoring/monitor-project/-/environments/1/additional_metrics.json"
>
- <alerts-deprecation-warning-stub />
-
<div
class="prometheus-graphs-header d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 border-bottom bg-gray-light"
>
diff --git a/spec/frontend/monitoring/components/alert_widget_form_spec.js b/spec/frontend/monitoring/components/alert_widget_form_spec.js
deleted file mode 100644
index e0ef1040f6b..00000000000
--- a/spec/frontend/monitoring/components/alert_widget_form_spec.js
+++ /dev/null
@@ -1,242 +0,0 @@
-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';
-
-describe('AlertWidgetForm', () => {
- let wrapper;
-
- const metricId = '8';
- const alertPath = 'alert';
- const relevantQueries = [{ metricId, alert_path: alertPath, label: 'alert-label' }];
- const dataTrackingOptions = {
- create: { action: 'click_button', label: 'create_alert' },
- delete: { action: 'click_button', label: 'delete_alert' },
- update: { action: 'click_button', label: 'update_alert' },
- };
-
- const defaultProps = {
- disabled: false,
- relevantQueries,
- modalId: 'alert-modal-1',
- };
-
- const propsWithAlertData = {
- ...defaultProps,
- alertsToManage: {
- alert: {
- alert_path: alertPath,
- operator: '<',
- threshold: 5,
- metricId,
- runbookUrl: INVALID_URL,
- },
- },
- configuredAlert: metricId,
- };
-
- function createComponent(props = {}) {
- const propsData = {
- ...defaultProps,
- ...props,
- };
-
- wrapper = shallowMount(AlertWidgetForm, {
- propsData,
- stubs: {
- GlModal: ModalStub,
- },
- });
- }
-
- const modal = () => wrapper.find(ModalStub);
- const modalTitle = () => modal().attributes('title');
- const submitButton = () => modal().find(GlLink);
- const findRunbookField = () => modal().find('[data-testid="alertRunbookField"]');
- const findThresholdField = () => modal().find('[data-qa-selector="alert_threshold_field"]');
- const submitButtonTrackingOpts = () =>
- JSON.parse(submitButton().attributes('data-tracking-options'));
- const stubEvent = { preventDefault: jest.fn() };
-
- afterEach(() => {
- if (wrapper) wrapper.destroy();
- });
-
- it('disables the form when disabled prop is set', () => {
- createComponent({ disabled: true });
-
- expect(modal().attributes('ok-disabled')).toBe('true');
- });
-
- it('disables the form if no query is selected', () => {
- createComponent();
-
- expect(modal().attributes('ok-disabled')).toBe('true');
- });
-
- it('shows correct title and button text', () => {
- createComponent();
-
- expect(modalTitle()).toBe('Add alert');
- expect(submitButton().text()).toBe('Add');
- });
-
- it('sets tracking options for create alert', () => {
- createComponent();
-
- expect(submitButtonTrackingOpts()).toEqual(dataTrackingOptions.create);
- });
-
- it('emits a "create" event when form submitted without existing alert', async () => {
- createComponent(defaultProps);
-
- modal().vm.$emit('shown');
-
- findThresholdField().vm.$emit('input', 900);
- findRunbookField().vm.$emit('input', INVALID_URL);
-
- modal().vm.$emit('ok', stubEvent);
-
- expect(wrapper.emitted().create[0]).toEqual([
- {
- alert: undefined,
- operator: '>',
- threshold: 900,
- prometheus_metric_id: '8',
- runbookUrl: INVALID_URL,
- },
- ]);
- });
-
- it('resets form when modal is dismissed (hidden)', () => {
- createComponent(defaultProps);
-
- modal().vm.$emit('shown');
-
- findThresholdField().vm.$emit('input', 800);
- findRunbookField().vm.$emit('input', INVALID_URL);
-
- modal().vm.$emit('hidden');
-
- expect(wrapper.vm.selectedAlert).toEqual({});
- expect(wrapper.vm.operator).toBe(null);
- expect(wrapper.vm.threshold).toBe(null);
- expect(wrapper.vm.prometheusMetricId).toBe(null);
- expect(wrapper.vm.runbookUrl).toBe(null);
- });
-
- it('sets selectedAlert to the provided configuredAlert on modal show', () => {
- createComponent(propsWithAlertData);
-
- modal().vm.$emit('shown');
-
- expect(wrapper.vm.selectedAlert).toEqual(propsWithAlertData.alertsToManage[alertPath]);
- });
-
- it('sets selectedAlert to the first relevantQueries if there is only one option on modal show', () => {
- createComponent({
- ...propsWithAlertData,
- configuredAlert: '',
- });
-
- modal().vm.$emit('shown');
-
- expect(wrapper.vm.selectedAlert).toEqual(propsWithAlertData.alertsToManage[alertPath]);
- });
-
- it('does not set selectedAlert to the first relevantQueries if there is more than one option on modal show', () => {
- createComponent({
- relevantQueries: [
- {
- metricId: '8',
- alertPath: 'alert',
- label: 'alert-label',
- },
- {
- metricId: '9',
- alertPath: 'alert',
- label: 'alert-label',
- },
- ],
- });
-
- modal().vm.$emit('shown');
-
- expect(wrapper.vm.selectedAlert).toEqual({});
- });
-
- describe('with existing alert', () => {
- beforeEach(() => {
- createComponent(propsWithAlertData);
-
- modal().vm.$emit('shown');
- });
-
- it('sets tracking options for delete alert', () => {
- expect(submitButtonTrackingOpts()).toEqual(dataTrackingOptions.delete);
- });
-
- it('updates button text', () => {
- expect(modalTitle()).toBe('Edit alert');
- expect(submitButton().text()).toBe('Delete');
- });
-
- it('emits "delete" event when form values unchanged', () => {
- modal().vm.$emit('ok', stubEvent);
-
- expect(wrapper.emitted().delete[0]).toEqual([
- {
- alert: 'alert',
- operator: '<',
- threshold: 5,
- prometheus_metric_id: '8',
- runbookUrl: INVALID_URL,
- },
- ]);
- });
- });
-
- it('emits "update" event when form changed', () => {
- const updatedRunbookUrl = `${INVALID_URL}/test`;
-
- createComponent(propsWithAlertData);
-
- modal().vm.$emit('shown');
-
- findRunbookField().vm.$emit('input', updatedRunbookUrl);
- findThresholdField().vm.$emit('input', 11);
-
- modal().vm.$emit('ok', stubEvent);
-
- expect(wrapper.emitted().update[0]).toEqual([
- {
- alert: 'alert',
- operator: '<',
- threshold: 11,
- prometheus_metric_id: '8',
- runbookUrl: updatedRunbookUrl,
- },
- ]);
- });
-
- it('sets tracking options for update alert', async () => {
- createComponent(propsWithAlertData);
-
- modal().vm.$emit('shown');
-
- findThresholdField().vm.$emit('input', 11);
-
- await wrapper.vm.$nextTick();
-
- expect(submitButtonTrackingOpts()).toEqual(dataTrackingOptions.update);
- });
-
- describe('alert runbooks', () => {
- it('shows the runbook field', () => {
- createComponent();
-
- expect(findRunbookField().exists()).toBe(true);
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/charts/anomaly_spec.js b/spec/frontend/monitoring/components/charts/anomaly_spec.js
index c44fd8dce33..8dc6132709e 100644
--- a/spec/frontend/monitoring/components/charts/anomaly_spec.js
+++ b/spec/frontend/monitoring/components/charts/anomaly_spec.js
@@ -159,10 +159,6 @@ describe('Anomaly chart component', () => {
const { deploymentData } = getTimeSeriesProps();
expect(deploymentData).toEqual(anomalyDeploymentData);
});
- it('"thresholds" keeps the same value', () => {
- const { thresholds } = getTimeSeriesProps();
- expect(thresholds).toEqual(inputThresholds);
- });
it('"projectPath" keeps the same value', () => {
const { projectPath } = getTimeSeriesProps();
expect(projectPath).toEqual(mockProjectPath);
diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js
index ea6e4f4a5ed..27f7489aa49 100644
--- a/spec/frontend/monitoring/components/charts/time_series_spec.js
+++ b/spec/frontend/monitoring/components/charts/time_series_spec.js
@@ -643,7 +643,6 @@ describe('Time series component', () => {
expect(props.data).toBe(wrapper.vm.chartData);
expect(props.option).toBe(wrapper.vm.chartOptions);
expect(props.formatTooltipText).toBe(wrapper.vm.formatTooltipText);
- expect(props.thresholds).toBe(wrapper.vm.thresholds);
});
it('receives a tooltip title', () => {
diff --git a/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js b/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
index 8af6075a416..400ac2e8f85 100644
--- a/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
@@ -28,7 +28,6 @@ describe('dashboard invalid url parameters', () => {
},
},
options,
- provide: { hasManagedPrometheus: false },
});
};
diff --git a/spec/frontend/monitoring/components/dashboard_panel_spec.js b/spec/frontend/monitoring/components/dashboard_panel_spec.js
index c8951dff9ed..9a73dc820af 100644
--- a/spec/frontend/monitoring/components/dashboard_panel_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_panel_spec.js
@@ -5,7 +5,6 @@ import Vuex from 'vuex';
import { setTestTimeout } from 'helpers/timeout';
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';
@@ -28,7 +27,6 @@ import {
barGraphData,
} from '../graph_data';
import {
- mockAlert,
mockLogsHref,
mockLogsPath,
mockNamespace,
@@ -56,7 +54,6 @@ describe('Dashboard Panel', () => {
const findCtxMenu = () => wrapper.find({ ref: 'contextualMenu' });
const findMenuItems = () => wrapper.findAll(GlDropdownItem);
const findMenuItemByText = (text) => findMenuItems().filter((i) => i.text() === text);
- const findAlertsWidget = () => wrapper.find(AlertWidget);
const createWrapper = (props, { mountFn = shallowMount, ...options } = {}) => {
wrapper = mountFn(DashboardPanel, {
@@ -80,9 +77,6 @@ describe('Dashboard Panel', () => {
});
};
- const setMetricsSavedToDb = (val) =>
- monitoringDashboard.getters.metricsSavedToDb.mockReturnValue(val);
-
beforeEach(() => {
setTestTimeout(1000);
@@ -601,42 +595,6 @@ describe('Dashboard Panel', () => {
});
});
- describe('panel alerts', () => {
- beforeEach(() => {
- mockGetterReturnValue('metricsSavedToDb', []);
-
- createWrapper();
- });
-
- describe.each`
- desc | metricsSavedToDb | props | isShown
- ${'with permission and no metrics in db'} | ${[]} | ${{}} | ${false}
- ${'with permission and related metrics in db'} | ${[graphData.metrics[0].metricId]} | ${{}} | ${true}
- ${'without permission and related metrics in db'} | ${[graphData.metrics[0].metricId]} | ${{ prometheusAlertsAvailable: false }} | ${false}
- ${'with permission and unrelated metrics in db'} | ${['another_metric_id']} | ${{}} | ${false}
- `('$desc', ({ metricsSavedToDb, isShown, props }) => {
- const showsDesc = isShown ? 'shows' : 'does not show';
-
- beforeEach(() => {
- setMetricsSavedToDb(metricsSavedToDb);
- createWrapper({
- alertsEndpoint: '/endpoint',
- prometheusAlertsAvailable: true,
- ...props,
- });
- return wrapper.vm.$nextTick();
- });
-
- it(`${showsDesc} alert widget`, () => {
- expect(findAlertsWidget().exists()).toBe(isShown);
- });
-
- it(`${showsDesc} alert configuration`, () => {
- expect(findMenuItemByText('Alerts').exists()).toBe(isShown);
- });
- });
- });
-
describe('When graphData contains links', () => {
const findManageLinksItem = () => wrapper.find({ ref: 'manageLinksItem' });
const mockLinks = [
@@ -730,13 +688,6 @@ describe('Dashboard Panel', () => {
describe('Runbook url', () => {
const findRunbookLinks = () => wrapper.findAll('[data-testid="runbookLink"]');
- const { metricId } = graphData.metrics[0];
- const { alert_path: alertPath } = mockAlert;
-
- const mockRunbookAlert = {
- ...mockAlert,
- metricId,
- };
beforeEach(() => {
mockGetterReturnValue('metricsSavedToDb', []);
@@ -747,62 +698,5 @@ describe('Dashboard Panel', () => {
expect(findRunbookLinks().length).toBe(0);
});
-
- describe('when alerts are present', () => {
- beforeEach(() => {
- setMetricsSavedToDb([metricId]);
-
- createWrapper({
- alertsEndpoint: '/endpoint',
- prometheusAlertsAvailable: true,
- });
- });
-
- it('does not show a runbook link when a runbook is not set', async () => {
- findAlertsWidget().vm.$emit('setAlerts', alertPath, {
- ...mockRunbookAlert,
- runbookUrl: '',
- });
-
- await wrapper.vm.$nextTick();
-
- expect(findRunbookLinks().length).toBe(0);
- });
-
- it('shows a runbook link when a runbook is set', async () => {
- findAlertsWidget().vm.$emit('setAlerts', alertPath, mockRunbookAlert);
-
- await wrapper.vm.$nextTick();
-
- expect(findRunbookLinks().length).toBe(1);
- expect(findRunbookLinks().at(0).attributes('href')).toBe(invalidUrl);
- });
- });
-
- describe('managed alert deprecation feature flag', () => {
- beforeEach(() => {
- setMetricsSavedToDb([metricId]);
- });
-
- it('shows alerts when alerts are not deprecated', () => {
- createWrapper(
- { alertsEndpoint: '/endpoint', prometheusAlertsAvailable: true },
- { provide: { glFeatures: { managedAlertsDeprecation: false } } },
- );
-
- expect(findAlertsWidget().exists()).toBe(true);
- expect(findMenuItemByText('Alerts').exists()).toBe(true);
- });
-
- it('hides alerts when alerts are deprecated', () => {
- createWrapper(
- { alertsEndpoint: '/endpoint', prometheusAlertsAvailable: true },
- { provide: { glFeatures: { managedAlertsDeprecation: true } } },
- );
-
- expect(findAlertsWidget().exists()).toBe(false);
- expect(findMenuItemByText('Alerts').exists()).toBe(false);
- });
- });
});
});
diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js
index f899580b3df..9331048bce3 100644
--- a/spec/frontend/monitoring/components/dashboard_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_spec.js
@@ -46,7 +46,6 @@ describe('Dashboard', () => {
stubs: {
DashboardHeader,
},
- provide: { hasManagedPrometheus: false },
...options,
});
};
@@ -60,9 +59,6 @@ describe('Dashboard', () => {
'dashboard-panel': true,
'dashboard-header': DashboardHeader,
},
- provide: {
- hasManagedPrometheus: false,
- },
...options,
});
};
@@ -412,7 +408,7 @@ describe('Dashboard', () => {
});
});
- describe('when all requests have been commited by the store', () => {
+ describe('when all requests have been committed by the store', () => {
beforeEach(() => {
store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
currentEnvironmentName: 'production',
@@ -460,7 +456,7 @@ describe('Dashboard', () => {
it('shows the links section', () => {
expect(wrapper.vm.shouldShowLinksSection).toBe(true);
- expect(wrapper.find(LinksSection)).toExist();
+ expect(wrapper.findComponent(LinksSection).exists()).toBe(true);
});
});
@@ -807,29 +803,4 @@ describe('Dashboard', () => {
expect(dashboardPanel.exists()).toBe(true);
});
});
-
- describe('alerts deprecation', () => {
- beforeEach(() => {
- setupStoreWithData(store);
- });
-
- const findDeprecationNotice = () => wrapper.findByTestId('alerts-deprecation-warning');
-
- it.each`
- managedAlertsDeprecation | hasManagedPrometheus | isVisible
- ${false} | ${false} | ${false}
- ${false} | ${true} | ${true}
- ${true} | ${false} | ${false}
- ${true} | ${true} | ${false}
- `(
- 'when the deprecation feature flag is $managedAlertsDeprecation and has managed prometheus is $hasManagedPrometheus',
- ({ hasManagedPrometheus, managedAlertsDeprecation, isVisible }) => {
- createMountedWrapper(
- {},
- { provide: { hasManagedPrometheus, glFeatures: { managedAlertsDeprecation } } },
- );
- expect(findDeprecationNotice().exists()).toBe(isVisible);
- },
- );
- });
});
diff --git a/spec/frontend/monitoring/components/dashboard_url_time_spec.js b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
index bea263f143a..e6785f34597 100644
--- a/spec/frontend/monitoring/components/dashboard_url_time_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
@@ -31,7 +31,6 @@ describe('dashboard invalid url parameters', () => {
store,
stubs: { 'graph-group': true, 'dashboard-panel': true, 'dashboard-header': DashboardHeader },
...options,
- provide: { hasManagedPrometheus: false },
});
};
diff --git a/spec/frontend/monitoring/components/links_section_spec.js b/spec/frontend/monitoring/components/links_section_spec.js
index 8fc287c50e4..e37abf6722a 100644
--- a/spec/frontend/monitoring/components/links_section_spec.js
+++ b/spec/frontend/monitoring/components/links_section_spec.js
@@ -1,5 +1,7 @@
import { GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+
import LinksSection from '~/monitoring/components/links_section.vue';
import { createStore } from '~/monitoring/stores';
@@ -26,12 +28,12 @@ describe('Links Section component', () => {
createShallowWrapper();
});
- it('does not render a section if no links are present', () => {
+ it('does not render a section if no links are present', async () => {
setState();
- return wrapper.vm.$nextTick(() => {
- expect(findLinks()).not.toExist();
- });
+ await nextTick();
+
+ expect(findLinks().length).toBe(0);
});
it('renders a link inside a section', () => {
diff --git a/spec/frontend/monitoring/components/variables/text_field_spec.js b/spec/frontend/monitoring/components/variables/text_field_spec.js
index 28e02dff4bf..c879803fddd 100644
--- a/spec/frontend/monitoring/components/variables/text_field_spec.js
+++ b/spec/frontend/monitoring/components/variables/text_field_spec.js
@@ -15,12 +15,12 @@ describe('Text variable component', () => {
});
};
- const findInput = () => wrapper.find(GlFormInput);
+ const findInput = () => wrapper.findComponent(GlFormInput);
it('renders a text input when all props are passed', () => {
createShallowWrapper();
- expect(findInput()).toExist();
+ expect(findInput().exists()).toBe(true);
});
it('always has a default value', () => {
diff --git a/spec/frontend/monitoring/pages/dashboard_page_spec.js b/spec/frontend/monitoring/pages/dashboard_page_spec.js
index dbe9cc21ad5..c5a8b50ee60 100644
--- a/spec/frontend/monitoring/pages/dashboard_page_spec.js
+++ b/spec/frontend/monitoring/pages/dashboard_page_spec.js
@@ -29,7 +29,7 @@ describe('monitoring/pages/dashboard_page', () => {
});
};
- const findDashboardComponent = () => wrapper.find(Dashboard);
+ const findDashboardComponent = () => wrapper.findComponent(Dashboard);
beforeEach(() => {
buildRouter();
@@ -60,7 +60,7 @@ describe('monitoring/pages/dashboard_page', () => {
smallEmptyState: false,
};
- expect(findDashboardComponent()).toExist();
+ expect(findDashboardComponent().exists()).toBe(true);
expect(allProps).toMatchObject(findDashboardComponent().props());
});
});
diff --git a/spec/frontend/monitoring/router_spec.js b/spec/frontend/monitoring/router_spec.js
index 2a712d4361f..b027d60f61e 100644
--- a/spec/frontend/monitoring/router_spec.js
+++ b/spec/frontend/monitoring/router_spec.js
@@ -20,8 +20,6 @@ const MockApp = {
template: `<router-view :dashboard-props="dashboardProps"/>`,
};
-const provide = { hasManagedPrometheus: false };
-
describe('Monitoring router', () => {
let router;
let store;
@@ -39,7 +37,6 @@ describe('Monitoring router', () => {
localVue,
store,
router,
- provide,
});
};
diff --git a/spec/frontend/notes/components/discussion_counter_spec.js b/spec/frontend/notes/components/discussion_counter_spec.js
index 9db0f823d84..c454d502beb 100644
--- a/spec/frontend/notes/components/discussion_counter_spec.js
+++ b/spec/frontend/notes/components/discussion_counter_spec.js
@@ -53,7 +53,7 @@ describe('DiscussionCounter component', () => {
describe('has no resolvable discussions', () => {
it('does not render', () => {
- store.commit(types.SET_INITIAL_DISCUSSIONS, [{ ...discussionMock, resolvable: false }]);
+ store.commit(types.ADD_OR_UPDATE_DISCUSSIONS, [{ ...discussionMock, resolvable: false }]);
store.dispatch('updateResolvableDiscussionsCounts');
wrapper = shallowMount(DiscussionCounter, { store, localVue });
@@ -64,7 +64,7 @@ describe('DiscussionCounter component', () => {
describe('has resolvable discussions', () => {
const updateStore = (note = {}) => {
discussionMock.notes[0] = { ...discussionMock.notes[0], ...note };
- store.commit(types.SET_INITIAL_DISCUSSIONS, [discussionMock]);
+ store.commit(types.ADD_OR_UPDATE_DISCUSSIONS, [discussionMock]);
store.dispatch('updateResolvableDiscussionsCounts');
};
@@ -97,7 +97,7 @@ describe('DiscussionCounter component', () => {
let toggleAllButton;
const updateStoreWithExpanded = (expanded) => {
const discussion = { ...discussionMock, expanded };
- store.commit(types.SET_INITIAL_DISCUSSIONS, [discussion]);
+ store.commit(types.ADD_OR_UPDATE_DISCUSSIONS, [discussion]);
store.dispatch('updateResolvableDiscussionsCounts');
wrapper = shallowMount(DiscussionCounter, { store, localVue });
toggleAllButton = wrapper.find('.toggle-all-discussions-btn');
diff --git a/spec/frontend/notes/components/discussion_notes_spec.js b/spec/frontend/notes/components/discussion_notes_spec.js
index 59ac75f00e6..ff840a55535 100644
--- a/spec/frontend/notes/components/discussion_notes_spec.js
+++ b/spec/frontend/notes/components/discussion_notes_spec.js
@@ -1,6 +1,7 @@
import { getByRole } from '@testing-library/dom';
import { shallowMount, mount } from '@vue/test-utils';
import '~/behaviors/markdown/render_gfm';
+import { discussionIntersectionObserverHandlerFactory } from '~/diffs/utils/discussions';
import DiscussionNotes from '~/notes/components/discussion_notes.vue';
import NoteableNote from '~/notes/components/noteable_note.vue';
import { SYSTEM_NOTE } from '~/notes/constants';
@@ -26,6 +27,9 @@ describe('DiscussionNotes', () => {
const createComponent = (props, mountingMethod = shallowMount) => {
wrapper = mountingMethod(DiscussionNotes, {
store,
+ provide: {
+ discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
+ },
propsData: {
discussion: discussionMock,
isExpanded: false,
diff --git a/spec/frontend/notes/components/multiline_comment_form_spec.js b/spec/frontend/notes/components/multiline_comment_form_spec.js
index b6d603c6358..b027a261c15 100644
--- a/spec/frontend/notes/components/multiline_comment_form_spec.js
+++ b/spec/frontend/notes/components/multiline_comment_form_spec.js
@@ -50,18 +50,6 @@ describe('MultilineCommentForm', () => {
expect(wrapper.vm.commentLineStart).toEqual(lineRange.start);
expect(setSelectedCommentPosition).toHaveBeenCalled();
});
-
- it('sets commentLineStart to selectedCommentPosition', () => {
- const notes = {
- selectedCommentPosition: {
- start: { ...testLine },
- },
- };
- const wrapper = createWrapper({}, { notes });
-
- expect(wrapper.vm.commentLineStart).toEqual(wrapper.vm.selectedCommentPosition.start);
- expect(setSelectedCommentPosition).not.toHaveBeenCalled();
- });
});
describe('destroyed', () => {
diff --git a/spec/frontend/notes/components/note_body_spec.js b/spec/frontend/notes/components/note_body_spec.js
index 40251244423..4e345c9ac8d 100644
--- a/spec/frontend/notes/components/note_body_spec.js
+++ b/spec/frontend/notes/components/note_body_spec.js
@@ -58,7 +58,6 @@ describe('issue_note_body component', () => {
it('adds autosave', () => {
const autosaveKey = `autosave/Note/${note.noteable_type}/${note.id}`;
- expect(vm.autosave).toExist();
expect(vm.autosave.key).toEqual(autosaveKey);
});
});
diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js
index abc888cd245..48bfd6eac5a 100644
--- a/spec/frontend/notes/components/note_form_spec.js
+++ b/spec/frontend/notes/components/note_form_spec.js
@@ -1,3 +1,4 @@
+import { GlLink } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import batchComments from '~/batch_comments/stores/modules/batch_comments';
@@ -91,6 +92,7 @@ describe('issue_note_form component', () => {
expect(conflictWarning.exists()).toBe(true);
expect(conflictWarning.text().replace(/\s+/g, ' ').trim()).toBe(message);
+ expect(conflictWarning.find(GlLink).attributes('href')).toBe('#note_545');
});
});
diff --git a/spec/frontend/notes/components/noteable_discussion_spec.js b/spec/frontend/notes/components/noteable_discussion_spec.js
index 727ef02dcbb..6aab60edc4e 100644
--- a/spec/frontend/notes/components/noteable_discussion_spec.js
+++ b/spec/frontend/notes/components/noteable_discussion_spec.js
@@ -3,6 +3,7 @@ import { nextTick } from 'vue';
import discussionWithTwoUnresolvedNotes from 'test_fixtures/merge_requests/resolved_diff_discussion.json';
import { trimText } from 'helpers/text_helper';
import mockDiffFile from 'jest/diffs/mock_data/diff_file';
+import { discussionIntersectionObserverHandlerFactory } from '~/diffs/utils/discussions';
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';
@@ -31,6 +32,9 @@ describe('noteable_discussion component', () => {
wrapper = mount(NoteableDiscussion, {
store,
+ provide: {
+ discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
+ },
propsData: { discussion: discussionMock },
});
});
@@ -167,6 +171,9 @@ describe('noteable_discussion component', () => {
wrapper = mount(NoteableDiscussion, {
store,
+ provide: {
+ discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
+ },
propsData: { discussion: discussionMock },
});
});
@@ -185,6 +192,9 @@ describe('noteable_discussion component', () => {
wrapper = mount(NoteableDiscussion, {
store,
+ provide: {
+ discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
+ },
propsData: { discussion: discussionMock },
});
});
diff --git a/spec/frontend/notes/components/notes_app_spec.js b/spec/frontend/notes/components/notes_app_spec.js
index 241a89b2218..b3dbc26878f 100644
--- a/spec/frontend/notes/components/notes_app_spec.js
+++ b/spec/frontend/notes/components/notes_app_spec.js
@@ -2,11 +2,14 @@ import { mount, shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import Vue from 'vue';
+import setWindowLocation from 'helpers/set_window_location_helper';
import { setTestTimeout } from 'helpers/timeout';
+import waitForPromises from 'helpers/wait_for_promises';
import DraftNote from '~/batch_comments/components/draft_note.vue';
import batchComments from '~/batch_comments/stores/modules/batch_comments';
import axios from '~/lib/utils/axios_utils';
import * as urlUtility from '~/lib/utils/url_utility';
+import { discussionIntersectionObserverHandlerFactory } from '~/diffs/utils/discussions';
import CommentForm from '~/notes/components/comment_form.vue';
import NotesApp from '~/notes/components/notes_app.vue';
import * as constants from '~/notes/constants';
@@ -76,6 +79,9 @@ describe('note_app', () => {
</div>`,
},
{
+ provide: {
+ discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
+ },
propsData,
store,
},
@@ -430,4 +436,57 @@ describe('note_app', () => {
);
});
});
+
+ describe('fetching discussions', () => {
+ describe('when note anchor is not present', () => {
+ it('does not include extra query params', async () => {
+ wrapper = shallowMount(NotesApp, { propsData, store: createStore() });
+ await waitForPromises();
+
+ expect(axiosMock.history.get[0].params).toBeUndefined();
+ });
+ });
+
+ describe('when note anchor is present', () => {
+ const mountWithNotesFilter = (notesFilter) =>
+ shallowMount(NotesApp, {
+ propsData: {
+ ...propsData,
+ notesData: {
+ ...propsData.notesData,
+ notesFilter,
+ },
+ },
+ store: createStore(),
+ });
+
+ beforeEach(() => {
+ setWindowLocation('#note_1');
+ });
+
+ it('does not include extra query params when filter is undefined', async () => {
+ wrapper = mountWithNotesFilter(undefined);
+ await waitForPromises();
+
+ expect(axiosMock.history.get[0].params).toBeUndefined();
+ });
+
+ it('does not include extra query params when filter is already set to default', async () => {
+ wrapper = mountWithNotesFilter(constants.DISCUSSION_FILTERS_DEFAULT_VALUE);
+ await waitForPromises();
+
+ expect(axiosMock.history.get[0].params).toBeUndefined();
+ });
+
+ it('includes extra query params when filter is not set to default', async () => {
+ wrapper = mountWithNotesFilter(constants.COMMENTS_ONLY_FILTER_VALUE);
+ await waitForPromises();
+
+ expect(axiosMock.history.get[0].params).toEqual({
+ notes_filter: constants.DISCUSSION_FILTERS_DEFAULT_VALUE,
+ persist_filter: false,
+ });
+ });
+ });
+ });
});
diff --git a/spec/frontend/notes/mixins/discussion_navigation_spec.js b/spec/frontend/notes/mixins/discussion_navigation_spec.js
index 6a6e47ffcc5..26a072b82f8 100644
--- a/spec/frontend/notes/mixins/discussion_navigation_spec.js
+++ b/spec/frontend/notes/mixins/discussion_navigation_spec.js
@@ -1,4 +1,5 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { nextTick } from 'vue';
import Vuex from 'vuex';
import { setHTMLFixture } from 'helpers/fixtures';
import createEventHub from '~/helpers/event_hub_factory';
@@ -7,12 +8,15 @@ import eventHub from '~/notes/event_hub';
import discussionNavigation from '~/notes/mixins/discussion_navigation';
import notesModule from '~/notes/stores/modules';
+let scrollToFile;
const discussion = (id, index) => ({
id,
resolvable: index % 2 === 0,
active: true,
notes: [{}],
diff_discussion: true,
+ position: { new_line: 1, old_line: 1 },
+ diff_file: { file_path: 'test.js' },
});
const createDiscussions = () => [...'abcde'].map(discussion);
const createComponent = () => ({
@@ -45,6 +49,7 @@ describe('Discussion navigation mixin', () => {
jest.spyOn(utils, 'scrollToElement');
expandDiscussion = jest.fn();
+ scrollToFile = jest.fn();
const { actions, ...notesRest } = notesModule();
store = new Vuex.Store({
modules: {
@@ -52,6 +57,10 @@ describe('Discussion navigation mixin', () => {
...notesRest,
actions: { ...actions, expandDiscussion },
},
+ diffs: {
+ namespaced: true,
+ actions: { scrollToFile },
+ },
},
});
store.state.notes.discussions = createDiscussions();
@@ -136,6 +145,7 @@ describe('Discussion navigation mixin', () => {
it('scrolls to element', () => {
expect(utils.scrollToElement).toHaveBeenCalledWith(
findDiscussion('div.discussion', expected),
+ { behavior: 'smooth' },
);
});
});
@@ -163,6 +173,7 @@ describe('Discussion navigation mixin', () => {
expect(utils.scrollToElementWithContext).toHaveBeenCalledWith(
findDiscussion('ul.notes', expected),
+ { behavior: 'smooth' },
);
});
});
@@ -203,10 +214,60 @@ describe('Discussion navigation mixin', () => {
it('scrolls to discussion', () => {
expect(utils.scrollToElement).toHaveBeenCalledWith(
findDiscussion('div.discussion', expected),
+ { behavior: 'smooth' },
);
});
});
});
});
+
+ describe.each`
+ diffsVirtualScrolling
+ ${false}
+ ${true}
+ `('virtual scrolling feature is $diffsVirtualScrolling', ({ diffsVirtualScrolling }) => {
+ beforeEach(() => {
+ window.gon = { features: { diffsVirtualScrolling } };
+
+ jest.spyOn(store, 'dispatch');
+
+ store.state.notes.currentDiscussionId = 'a';
+ window.location.hash = 'test';
+ });
+
+ afterEach(() => {
+ window.gon = {};
+ window.location.hash = '';
+ });
+
+ it('resets location hash if diffsVirtualScrolling flag is true', async () => {
+ wrapper.vm.jumpToNextDiscussion();
+
+ await nextTick();
+
+ expect(window.location.hash).toBe(diffsVirtualScrolling ? '' : '#test');
+ });
+
+ it.each`
+ tabValue | hashValue
+ ${'diffs'} | ${false}
+ ${'show'} | ${!diffsVirtualScrolling}
+ ${'other'} | ${!diffsVirtualScrolling}
+ `(
+ 'calls scrollToFile with setHash as $hashValue when the tab is $tabValue',
+ async ({ hashValue, tabValue }) => {
+ window.mrTabs.currentAction = tabValue;
+
+ wrapper.vm.jumpToNextDiscussion();
+
+ await nextTick();
+
+ expect(store.dispatch).toHaveBeenCalledWith('diffs/scrollToFile', {
+ path: 'test.js',
+ setHash: hashValue,
+ });
+ },
+ );
+ });
});
});
diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js
index 2ff65d3f47e..bbe074f0105 100644
--- a/spec/frontend/notes/stores/actions_spec.js
+++ b/spec/frontend/notes/stores/actions_spec.js
@@ -119,7 +119,7 @@ describe('Actions Notes Store', () => {
actions.setInitialNotes,
[individualNote],
{ notes: [] },
- [{ type: 'SET_INITIAL_DISCUSSIONS', payload: [individualNote] }],
+ [{ type: 'ADD_OR_UPDATE_DISCUSSIONS', payload: [individualNote] }],
[],
done,
);
@@ -1395,4 +1395,93 @@ describe('Actions Notes Store', () => {
);
});
});
+
+ describe('fetchDiscussions', () => {
+ const discussion = { notes: [] };
+
+ afterEach(() => {
+ window.gon = {};
+ });
+
+ it('updates the discussions and dispatches `updateResolvableDiscussionsCounts`', (done) => {
+ axiosMock.onAny().reply(200, { discussion });
+ testAction(
+ actions.fetchDiscussions,
+ {},
+ null,
+ [
+ { type: mutationTypes.ADD_OR_UPDATE_DISCUSSIONS, payload: { discussion } },
+ { type: mutationTypes.SET_FETCHING_DISCUSSIONS, payload: false },
+ ],
+ [{ type: 'updateResolvableDiscussionsCounts' }],
+ done,
+ );
+ });
+
+ it('dispatches `fetchDiscussionsBatch` action if `paginatedIssueDiscussions` feature flag is enabled', (done) => {
+ window.gon = { features: { paginatedIssueDiscussions: true } };
+
+ testAction(
+ actions.fetchDiscussions,
+ { path: 'test-path', filter: 'test-filter', persistFilter: 'test-persist-filter' },
+ null,
+ [],
+ [
+ {
+ type: 'fetchDiscussionsBatch',
+ payload: {
+ config: {
+ params: { notes_filter: 'test-filter', persist_filter: 'test-persist-filter' },
+ },
+ path: 'test-path',
+ perPage: 20,
+ },
+ },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('fetchDiscussionsBatch', () => {
+ const discussion = { notes: [] };
+
+ const config = {
+ params: { notes_filter: 'test-filter', persist_filter: 'test-persist-filter' },
+ };
+
+ const actionPayload = { config, path: 'test-path', perPage: 20 };
+
+ it('updates the discussions and dispatches `updateResolvableDiscussionsCounts if there are no headers', (done) => {
+ axiosMock.onAny().reply(200, { discussion }, {});
+ testAction(
+ actions.fetchDiscussionsBatch,
+ actionPayload,
+ null,
+ [
+ { type: mutationTypes.ADD_OR_UPDATE_DISCUSSIONS, payload: { discussion } },
+ { type: mutationTypes.SET_FETCHING_DISCUSSIONS, payload: false },
+ ],
+ [{ type: 'updateResolvableDiscussionsCounts' }],
+ done,
+ );
+ });
+
+ it('dispatches itself if there is `x-next-page-cursor` header', (done) => {
+ axiosMock.onAny().reply(200, { discussion }, { 'x-next-page-cursor': 1 });
+ testAction(
+ actions.fetchDiscussionsBatch,
+ actionPayload,
+ null,
+ [{ type: mutationTypes.ADD_OR_UPDATE_DISCUSSIONS, payload: { discussion } }],
+ [
+ {
+ type: 'fetchDiscussionsBatch',
+ payload: { ...actionPayload, perPage: 30, cursor: 1 },
+ },
+ ],
+ done,
+ );
+ });
+ });
});
diff --git a/spec/frontend/notes/stores/mutation_spec.js b/spec/frontend/notes/stores/mutation_spec.js
index 99e24f724f4..c9e24039b64 100644
--- a/spec/frontend/notes/stores/mutation_spec.js
+++ b/spec/frontend/notes/stores/mutation_spec.js
@@ -159,7 +159,7 @@ describe('Notes Store mutations', () => {
});
});
- describe('SET_INITIAL_DISCUSSIONS', () => {
+ describe('ADD_OR_UPDATE_DISCUSSIONS', () => {
it('should set the initial notes received', () => {
const state = {
discussions: [],
@@ -169,15 +169,17 @@ describe('Notes Store mutations', () => {
individual_note: true,
notes: [
{
+ id: 100,
note: '1',
},
{
+ id: 101,
note: '2',
},
],
};
- mutations.SET_INITIAL_DISCUSSIONS(state, [note, legacyNote]);
+ mutations.ADD_OR_UPDATE_DISCUSSIONS(state, [note, legacyNote]);
expect(state.discussions[0].id).toEqual(note.id);
expect(state.discussions[1].notes[0].note).toBe(legacyNote.notes[0].note);
@@ -190,7 +192,7 @@ describe('Notes Store mutations', () => {
discussions: [],
};
- mutations.SET_INITIAL_DISCUSSIONS(state, [
+ mutations.ADD_OR_UPDATE_DISCUSSIONS(state, [
{
...note,
diff_file: {
@@ -208,7 +210,7 @@ describe('Notes Store mutations', () => {
discussions: [],
};
- mutations.SET_INITIAL_DISCUSSIONS(state, [
+ mutations.ADD_OR_UPDATE_DISCUSSIONS(state, [
{
...note,
diff_file: {
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 dbebdeeb452..67e2594d29f 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
@@ -2,11 +2,11 @@
exports[`packages_list_app renders 1`] = `
<div>
- <div
- help-url="foo"
+ <infrastructure-title-stub
+ helpurl="foo"
/>
- <div />
+ <infrastructure-search-stub />
<div>
<section
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 b94192c531c..5f7555a3a2b 100644
--- a/spec/frontend/packages/list/components/packages_list_app_spec.js
+++ b/spec/frontend/packages/list/components/packages_list_app_spec.js
@@ -9,6 +9,7 @@ import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages/list/constants';
import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import * as packageUtils from '~/packages_and_registries/shared/utils';
+import InfrastructureSearch from '~/packages_and_registries/infrastructure_registry/components/infrastructure_search.vue';
jest.mock('~/lib/utils/common_utils');
jest.mock('~/flash');
@@ -26,18 +27,9 @@ describe('packages_list_app', () => {
};
const GlLoadingIcon = { name: 'gl-loading-icon', template: '<div>loading</div>' };
- // we need to manually stub dynamic imported components because shallowMount is not able to stub them automatically. See: https://github.com/vuejs/vue-test-utils/issues/1279
- const PackageSearch = { name: 'PackageSearch', template: '<div></div>' };
- const PackageTitle = { name: 'PackageTitle', template: '<div></div>' };
- const InfrastructureTitle = { name: 'InfrastructureTitle', template: '<div></div>' };
- const InfrastructureSearch = { name: 'InfrastructureSearch', template: '<div></div>' };
-
const emptyListHelpUrl = 'helpUrl';
const findEmptyState = () => wrapper.find(GlEmptyState);
const findListComponent = () => wrapper.find(PackageList);
- const findPackageSearch = () => wrapper.find(PackageSearch);
- const findPackageTitle = () => wrapper.find(PackageTitle);
- const findInfrastructureTitle = () => wrapper.find(InfrastructureTitle);
const findInfrastructureSearch = () => wrapper.find(InfrastructureSearch);
const createStore = (filter = []) => {
@@ -66,10 +58,6 @@ describe('packages_list_app', () => {
PackageList,
GlSprintf,
GlLink,
- PackageSearch,
- PackageTitle,
- InfrastructureTitle,
- InfrastructureSearch,
},
provide,
});
@@ -191,48 +179,23 @@ describe('packages_list_app', () => {
});
});
- describe('Package Search', () => {
+ describe('Search', () => {
it('exists', () => {
mountComponent();
- expect(findPackageSearch().exists()).toBe(true);
+ expect(findInfrastructureSearch().exists()).toBe(true);
});
it('on update fetches data from the store', () => {
mountComponent();
store.dispatch.mockClear();
- findPackageSearch().vm.$emit('update');
+ findInfrastructureSearch().vm.$emit('update');
expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList');
});
});
- describe('Infrastructure config', () => {
- it('defaults to package registry components', () => {
- mountComponent();
-
- expect(findPackageSearch().exists()).toBe(true);
- expect(findPackageTitle().exists()).toBe(true);
-
- expect(findInfrastructureTitle().exists()).toBe(false);
- expect(findInfrastructureSearch().exists()).toBe(false);
- });
-
- it('mount different component based on the provided values', () => {
- mountComponent({
- titleComponent: 'InfrastructureTitle',
- searchComponent: 'InfrastructureSearch',
- });
-
- expect(findPackageSearch().exists()).toBe(false);
- expect(findPackageTitle().exists()).toBe(false);
-
- expect(findInfrastructureTitle().exists()).toBe(true);
- expect(findInfrastructureSearch().exists()).toBe(true);
- });
- });
-
describe('delete alert handling', () => {
const originalLocation = window.location.href;
const search = `?${SHOW_DELETE_SUCCESS_ALERT}=true`;
diff --git a/spec/frontend/packages/list/components/packages_search_spec.js b/spec/frontend/packages/list/components/packages_search_spec.js
deleted file mode 100644
index 30fad74b493..00000000000
--- a/spec/frontend/packages/list/components/packages_search_spec.js
+++ /dev/null
@@ -1,128 +0,0 @@
-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 { sortableFields } from '~/packages/list/utils';
-import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
-import UrlSync from '~/vue_shared/components/url_sync.vue';
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
-describe('Package Search', () => {
- let wrapper;
- let store;
-
- const findRegistrySearch = () => wrapper.findComponent(RegistrySearch);
- const findUrlSync = () => wrapper.findComponent(UrlSync);
-
- 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,
- stubs: {
- UrlSync,
- },
- });
- };
-
- 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: sortableFields(),
- });
- });
-
- 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: sortableFields(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([[]]);
- });
-
- it('has a UrlSync component', () => {
- mountComponent();
-
- expect(findUrlSync().exists()).toBe(true);
- });
-
- it('on query:changed calls updateQuery from UrlSync', () => {
- jest.spyOn(UrlSync.methods, 'updateQuery').mockImplementation(() => {});
-
- mountComponent();
-
- findRegistrySearch().vm.$emit('query:changed');
-
- expect(UrlSync.methods.updateQuery).toHaveBeenCalled();
- });
-});
diff --git a/spec/frontend/packages/list/components/packages_title_spec.js b/spec/frontend/packages/list/components/packages_title_spec.js
deleted file mode 100644
index a17f72e3133..00000000000
--- a/spec/frontend/packages/list/components/packages_title_spec.js
+++ /dev/null
@@ -1,71 +0,0 @@
-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 MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
-import TitleArea from '~/vue_shared/components/registry/title_area.vue';
-
-describe('PackageTitle', () => {
- let wrapper;
- let store;
-
- const findTitleArea = () => wrapper.find(TitleArea);
- const findMetadataItem = () => wrapper.find(MetadataItem);
-
- const mountComponent = (propsData = { helpUrl: 'foo' }) => {
- wrapper = shallowMount(PackageTitle, {
- store,
- propsData,
- stubs: {
- TitleArea,
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- describe('title area', () => {
- it('exists', () => {
- mountComponent();
-
- expect(findTitleArea().exists()).toBe(true);
- });
-
- it('has the correct props', () => {
- mountComponent();
-
- expect(findTitleArea().props()).toMatchObject({
- title: LIST_TITLE_TEXT,
- infoMessages: [{ text: LIST_INTRO_TEXT, link: 'foo' }],
- });
- });
- });
-
- describe.each`
- count | exist | text
- ${null} | ${false} | ${''}
- ${undefined} | ${false} | ${''}
- ${0} | ${true} | ${'0 Packages'}
- ${1} | ${true} | ${'1 Package'}
- ${2} | ${true} | ${'2 Packages'}
- `('when count is $count metadata item', ({ count, exist, text }) => {
- beforeEach(() => {
- mountComponent({ count, helpUrl: 'foo' });
- });
-
- it(`is ${exist} that it exists`, () => {
- expect(findMetadataItem().exists()).toBe(exist);
- });
-
- if (exist) {
- it('has the correct props', () => {
- expect(findMetadataItem().props()).toMatchObject({
- icon: 'package',
- text,
- });
- });
- }
- });
-});
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
deleted file mode 100644
index b0cbe34f0b9..00000000000
--- a/spec/frontend/packages/list/components/tokens/package_type_token_spec.js
+++ /dev/null
@@ -1,48 +0,0 @@
-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/registry/explorer/components/__snapshots__/registry_breadcrumb_spec.js.snap b/spec/frontend/packages_and_registries/container_registry/explorer/components/__snapshots__/registry_breadcrumb_spec.js.snap
index f80e2ce6ecc..7044c1285d8 100644
--- a/spec/frontend/registry/explorer/components/__snapshots__/registry_breadcrumb_spec.js.snap
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/__snapshots__/registry_breadcrumb_spec.js.snap
@@ -4,10 +4,10 @@ exports[`Registry Breadcrumb when is not rootRoute renders 1`] = `
<div
class="gl-breadcrumbs"
>
+
<ol
class="breadcrumb gl-breadcrumb-list"
>
-
<li
class="breadcrumb-item gl-breadcrumb-item"
>
@@ -15,24 +15,28 @@ exports[`Registry Breadcrumb when is not rootRoute renders 1`] = `
class=""
href="/"
target="_self"
- />
- </li>
-
- <span
- class="gl-breadcrumb-separator"
- data-testid="separator"
- >
- <svg
- aria-hidden="true"
- class="gl-icon s8"
- data-testid="angle-right-icon"
- role="img"
>
- <use
- href="#angle-right"
- />
- </svg>
- </span>
+ <span>
+
+ </span>
+
+ <span
+ class="gl-breadcrumb-separator"
+ data-testid="separator"
+ >
+ <svg
+ aria-hidden="true"
+ class="gl-icon s8"
+ data-testid="angle-right-icon"
+ role="img"
+ >
+ <use
+ href="#angle-right"
+ />
+ </svg>
+ </span>
+ </a>
+ </li>
<li
class="breadcrumb-item gl-breadcrumb-item"
>
@@ -40,10 +44,14 @@ exports[`Registry Breadcrumb when is not rootRoute renders 1`] = `
class=""
href="#"
target="_self"
- />
+ >
+ <span>
+
+ </span>
+
+ <!---->
+ </a>
</li>
-
- <!---->
</ol>
</div>
`;
@@ -52,10 +60,10 @@ exports[`Registry Breadcrumb when is rootRoute renders 1`] = `
<div
class="gl-breadcrumbs"
>
+
<ol
class="breadcrumb gl-breadcrumb-list"
>
-
<li
class="breadcrumb-item gl-breadcrumb-item"
>
@@ -63,10 +71,14 @@ exports[`Registry Breadcrumb when is rootRoute renders 1`] = `
class=""
href="/"
target="_self"
- />
+ >
+ <span>
+
+ </span>
+
+ <!---->
+ </a>
</li>
-
- <!---->
</ol>
</div>
`;
diff --git a/spec/frontend/registry/explorer/components/delete_button_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_button_spec.js
index 4597c42add9..6d7bf528495 100644
--- a/spec/frontend/registry/explorer/components/delete_button_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_button_spec.js
@@ -1,7 +1,7 @@
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';
+import component from '~/packages_and_registries/container_registry/explorer/components/delete_button.vue';
describe('delete_button', () => {
let wrapper;
diff --git a/spec/frontend/registry/explorer/components/delete_image_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_image_spec.js
index 9a0d070e42b..620c96e8c9e 100644
--- a/spec/frontend/registry/explorer/components/delete_image_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_image_spec.js
@@ -1,9 +1,9 @@
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';
+import component from '~/packages_and_registries/container_registry/explorer/components/delete_image.vue';
+import { GRAPHQL_PAGE_SIZE } from '~/packages_and_registries/container_registry/explorer/constants/index';
+import deleteContainerRepositoryMutation from '~/packages_and_registries/container_registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql';
+import getContainerRepositoryDetailsQuery from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql';
describe('Delete Image', () => {
let wrapper;
diff --git a/spec/frontend/registry/explorer/components/details_page/__snapshots__/tags_loader_spec.js.snap b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/__snapshots__/tags_loader_spec.js.snap
index 5f191ef5561..5f191ef5561 100644
--- a/spec/frontend/registry/explorer/components/details_page/__snapshots__/tags_loader_spec.js.snap
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/__snapshots__/tags_loader_spec.js.snap
diff --git a/spec/frontend/registry/explorer/components/details_page/delete_alert_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js
index c2a2a4e06ea..e25162f4da5 100644
--- a/spec/frontend/registry/explorer/components/details_page/delete_alert_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js
@@ -1,13 +1,13 @@
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 component from '~/packages_and_registries/container_registry/explorer/components/details_page/delete_alert.vue';
import {
DELETE_TAG_SUCCESS_MESSAGE,
DELETE_TAG_ERROR_MESSAGE,
DELETE_TAGS_SUCCESS_MESSAGE,
DELETE_TAGS_ERROR_MESSAGE,
ADMIN_GARBAGE_COLLECTION_TIP,
-} from '~/registry/explorer/constants';
+} from '~/packages_and_registries/container_registry/explorer/constants';
describe('Delete alert', () => {
let wrapper;
diff --git a/spec/frontend/registry/explorer/components/details_page/delete_modal_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_modal_spec.js
index d2fe5af3a94..16c9485e69e 100644
--- a/spec/frontend/registry/explorer/components/details_page/delete_modal_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_modal_spec.js
@@ -1,13 +1,13 @@
import { GlSprintf, GlFormInput } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import component from '~/registry/explorer/components/details_page/delete_modal.vue';
+import component from '~/packages_and_registries/container_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';
+} from '~/packages_and_registries/container_registry/explorer/constants';
import { GlModal } from '../../stubs';
describe('Delete Modal', () => {
diff --git a/spec/frontend/registry/explorer/components/details_page/details_header_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js
index acff5c21940..f06300efa29 100644
--- a/spec/frontend/registry/explorer/components/details_page/details_header_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js
@@ -1,12 +1,12 @@
import { GlDropdownItem, GlIcon } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
+import { GlDropdown } from 'jest/packages_and_registries/container_registry/explorer/stubs';
import { useFakeDate } from 'helpers/fake_date';
import createMockApollo from 'helpers/mock_apollo_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import waitForPromises from 'helpers/wait_for_promises';
-import { GlDropdown } from 'jest/registry/explorer/stubs';
-import component from '~/registry/explorer/components/details_page/details_header.vue';
+import component from '~/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue';
import {
UNSCHEDULED_STATUS,
SCHEDULED_STATUS,
@@ -19,8 +19,8 @@ import {
CLEANUP_UNFINISHED_TOOLTIP,
ROOT_IMAGE_TEXT,
ROOT_IMAGE_TOOLTIP,
-} from '~/registry/explorer/constants';
-import getContainerRepositoryTagCountQuery from '~/registry/explorer/graphql/queries/get_container_repository_tags_count.query.graphql';
+} from '~/packages_and_registries/container_registry/explorer/constants';
+import getContainerRepositoryTagCountQuery from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags_count.query.graphql';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import { imageTagsCountMock } from '../../mock_data';
diff --git a/spec/frontend/registry/explorer/components/details_page/empty_state_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/empty_state_spec.js
index 14b15945631..f14284e9efe 100644
--- a/spec/frontend/registry/explorer/components/details_page/empty_state_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/empty_state_spec.js
@@ -1,12 +1,12 @@
import { GlEmptyState } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import component from '~/registry/explorer/components/details_page/empty_state.vue';
+import component from '~/packages_and_registries/container_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';
+} from '~/packages_and_registries/container_registry/explorer/constants';
describe('EmptyTagsState component', () => {
let wrapper;
diff --git a/spec/frontend/registry/explorer/components/details_page/partial_cleanup_alert_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert_spec.js
index af8a23e412c..1a27481a828 100644
--- a/spec/frontend/registry/explorer/components/details_page/partial_cleanup_alert_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert_spec.js
@@ -1,7 +1,10 @@
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';
+import component from '~/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert.vue';
+import {
+ DELETE_ALERT_TITLE,
+ DELETE_ALERT_LINK_TEXT,
+} from '~/packages_and_registries/container_registry/explorer/constants';
describe('Partial Cleanup alert', () => {
let wrapper;
diff --git a/spec/frontend/registry/explorer/components/details_page/status_alert_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/status_alert_spec.js
index b079883cefd..a11b102d9a6 100644
--- a/spec/frontend/registry/explorer/components/details_page/status_alert_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/status_alert_spec.js
@@ -1,6 +1,6 @@
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 component from '~/packages_and_registries/container_registry/explorer/components/details_page/status_alert.vue';
import {
DELETE_SCHEDULED,
DELETE_FAILED,
@@ -9,7 +9,7 @@ import {
SCHEDULED_FOR_DELETION_STATUS_MESSAGE,
FAILED_DELETION_STATUS_TITLE,
FAILED_DELETION_STATUS_MESSAGE,
-} from '~/registry/explorer/constants';
+} from '~/packages_and_registries/container_registry/explorer/constants';
describe('Status Alert', () => {
let wrapper;
diff --git a/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js
index a5da37a2786..00b1d03b7c2 100644
--- a/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js
@@ -4,13 +4,13 @@ import { nextTick } from 'vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import component from '~/registry/explorer/components/details_page/tags_list_row.vue';
+import component from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue';
import {
REMOVE_TAG_BUTTON_TITLE,
MISSING_MANIFEST_WARNING_TOOLTIP,
NOT_AVAILABLE_TEXT,
NOT_AVAILABLE_SIZE,
-} from '~/registry/explorer/constants/index';
+} from '~/packages_and_registries/container_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';
diff --git a/spec/frontend/registry/explorer/components/details_page/tags_list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
index 51934cd074d..9a42c82d7e0 100644
--- a/spec/frontend/registry/explorer/components/details_page/tags_list_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
@@ -4,12 +4,15 @@ import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import EmptyTagsState from '~/registry/explorer/components/details_page/empty_state.vue';
-import component from '~/registry/explorer/components/details_page/tags_list.vue';
-import TagsListRow from '~/registry/explorer/components/details_page/tags_list_row.vue';
-import TagsLoader from '~/registry/explorer/components/details_page/tags_loader.vue';
-import { TAGS_LIST_TITLE, REMOVE_TAGS_BUTTON_TITLE } from '~/registry/explorer/constants/index';
-import getContainerRepositoryTagsQuery from '~/registry/explorer/graphql/queries/get_container_repository_tags.query.graphql';
+import EmptyTagsState from '~/packages_and_registries/container_registry/explorer/components/details_page/empty_state.vue';
+import component from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue';
+import TagsListRow from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue';
+import TagsLoader from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_loader.vue';
+import {
+ TAGS_LIST_TITLE,
+ REMOVE_TAGS_BUTTON_TITLE,
+} from '~/packages_and_registries/container_registry/explorer/constants/index';
+import getContainerRepositoryTagsQuery from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql';
import { tagsMock, imageTagsMock, tagsPageInfo } from '../../mock_data';
const localVue = createLocalVue();
diff --git a/spec/frontend/registry/explorer/components/details_page/tags_loader_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_loader_spec.js
index 40d84d9d4a5..060dc9dc5f3 100644
--- a/spec/frontend/registry/explorer/components/details_page/tags_loader_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_loader_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import component from '~/registry/explorer/components/details_page/tags_loader.vue';
+import component from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_loader.vue';
import { GlSkeletonLoader } from '../../stubs';
describe('TagsLoader component', () => {
diff --git a/spec/frontend/registry/explorer/components/list_page/__snapshots__/group_empty_state_spec.js.snap b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/__snapshots__/group_empty_state_spec.js.snap
index 56579847468..56579847468 100644
--- a/spec/frontend/registry/explorer/components/list_page/__snapshots__/group_empty_state_spec.js.snap
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/__snapshots__/group_empty_state_spec.js.snap
diff --git a/spec/frontend/registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap
index 46b07b4c2d6..46b07b4c2d6 100644
--- a/spec/frontend/registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap
diff --git a/spec/frontend/registry/explorer/components/list_page/cleanup_status_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js
index 8f2c049a357..e8ddad2d8ca 100644
--- a/spec/frontend/registry/explorer/components/list_page/cleanup_status_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js
@@ -1,6 +1,6 @@
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import CleanupStatus from '~/registry/explorer/components/list_page/cleanup_status.vue';
+import CleanupStatus from '~/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue';
import {
CLEANUP_TIMED_OUT_ERROR_MESSAGE,
CLEANUP_STATUS_SCHEDULED,
@@ -10,7 +10,7 @@ import {
UNSCHEDULED_STATUS,
SCHEDULED_STATUS,
ONGOING_STATUS,
-} from '~/registry/explorer/constants';
+} from '~/packages_and_registries/container_registry/explorer/constants';
describe('cleanup_status', () => {
let wrapper;
diff --git a/spec/frontend/registry/explorer/components/list_page/cli_commands_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cli_commands_spec.js
index 8ca8fca65ed..4039fba869b 100644
--- a/spec/frontend/registry/explorer/components/list_page/cli_commands_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cli_commands_spec.js
@@ -1,7 +1,7 @@
import { GlDropdown } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import QuickstartDropdown from '~/registry/explorer/components/list_page/cli_commands.vue';
+import QuickstartDropdown from '~/packages_and_registries/container_registry/explorer/components/list_page/cli_commands.vue';
import {
QUICK_START,
LOGIN_COMMAND_LABEL,
@@ -10,7 +10,7 @@ import {
COPY_BUILD_TITLE,
PUSH_COMMAND_LABEL,
COPY_PUSH_TITLE,
-} from '~/registry/explorer/constants';
+} from '~/packages_and_registries/container_registry/explorer/constants';
import Tracking from '~/tracking';
import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
diff --git a/spec/frontend/registry/explorer/components/list_page/group_empty_state_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state_spec.js
index 989a60625e2..027cdf732bc 100644
--- a/spec/frontend/registry/explorer/components/list_page/group_empty_state_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state_spec.js
@@ -1,7 +1,7 @@
import { GlSprintf } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import groupEmptyState from '~/registry/explorer/components/list_page/group_empty_state.vue';
+import groupEmptyState from '~/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state.vue';
import { GlEmptyState } from '../../stubs';
const localVue = createLocalVue();
diff --git a/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js
index db0f869ab52..411bef54e40 100644
--- a/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js
@@ -2,9 +2,9 @@ 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 DeleteButton from '~/registry/explorer/components/delete_button.vue';
-import CleanupStatus from '~/registry/explorer/components/list_page/cleanup_status.vue';
-import Component from '~/registry/explorer/components/list_page/image_list_row.vue';
+import DeleteButton from '~/packages_and_registries/container_registry/explorer/components/delete_button.vue';
+import CleanupStatus from '~/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue';
+import Component from '~/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue';
import {
ROW_SCHEDULED_FOR_DELETION,
LIST_DELETE_BUTTON_DISABLED,
@@ -12,7 +12,7 @@ import {
IMAGE_DELETE_SCHEDULED_STATUS,
SCHEDULED_STATUS,
ROOT_IMAGE_TEXT,
-} from '~/registry/explorer/constants';
+} from '~/packages_and_registries/container_registry/explorer/constants';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import { imagesListResponse } from '../../mock_data';
diff --git a/spec/frontend/registry/explorer/components/list_page/image_list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_spec.js
index d7dd825ca3e..e0119954ed4 100644
--- a/spec/frontend/registry/explorer/components/list_page/image_list_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_spec.js
@@ -1,7 +1,7 @@
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';
+import Component from '~/packages_and_registries/container_registry/explorer/components/list_page/image_list.vue';
+import ImageListRow from '~/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue';
import { imagesListResponse, pageInfo as defaultPageInfo } from '../../mock_data';
diff --git a/spec/frontend/registry/explorer/components/list_page/project_empty_state_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state_spec.js
index 111aa45f231..21748ae2813 100644
--- a/spec/frontend/registry/explorer/components/list_page/project_empty_state_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state_spec.js
@@ -1,7 +1,7 @@
import { GlSprintf } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import projectEmptyState from '~/registry/explorer/components/list_page/project_empty_state.vue';
+import projectEmptyState from '~/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state.vue';
import { dockerCommands } from '../../mock_data';
import { GlEmptyState } from '../../stubs';
diff --git a/spec/frontend/registry/explorer/components/list_page/registry_header_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js
index 11a3acd9eb9..92cfeb7633e 100644
--- a/spec/frontend/registry/explorer/components/list_page/registry_header_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js
@@ -1,11 +1,11 @@
import { GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Component from '~/registry/explorer/components/list_page/registry_header.vue';
+import Component from '~/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue';
import {
CONTAINER_REGISTRY_TITLE,
LIST_INTRO_TEXT,
EXPIRATION_POLICY_DISABLED_TEXT,
-} from '~/registry/explorer/constants';
+} from '~/packages_and_registries/container_registry/explorer/constants';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
jest.mock('~/lib/utils/datetime_utility', () => ({
diff --git a/spec/frontend/registry/explorer/components/registry_breadcrumb_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/registry_breadcrumb_spec.js
index 487f33594c1..e5a8438f23f 100644
--- a/spec/frontend/registry/explorer/components/registry_breadcrumb_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/registry_breadcrumb_spec.js
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
-import component from '~/registry/explorer/components/registry_breadcrumb.vue';
+import component from '~/packages_and_registries/container_registry/explorer/components/registry_breadcrumb.vue';
describe('Registry Breadcrumb', () => {
let wrapper;
diff --git a/spec/frontend/registry/explorer/mock_data.js b/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js
index 6a835a28807..6a835a28807 100644
--- a/spec/frontend/registry/explorer/mock_data.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js
diff --git a/spec/frontend/registry/explorer/pages/details_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
index 21af9dcc60f..adc9a64e5c9 100644
--- a/spec/frontend/registry/explorer/pages/details_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
@@ -4,14 +4,14 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
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 DetailsHeader from '~/registry/explorer/components/details_page/details_header.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 TagsLoader from '~/registry/explorer/components/details_page/tags_loader.vue';
+import DeleteImage from '~/packages_and_registries/container_registry/explorer/components/delete_image.vue';
+import DeleteAlert from '~/packages_and_registries/container_registry/explorer/components/details_page/delete_alert.vue';
+import DetailsHeader from '~/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue';
+import EmptyTagsState from '~/packages_and_registries/container_registry/explorer/components/details_page/empty_state.vue';
+import PartialCleanupAlert from '~/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert.vue';
+import StatusAlert from '~/packages_and_registries/container_registry/explorer/components/details_page/status_alert.vue';
+import TagsList from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue';
+import TagsLoader from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_loader.vue';
import {
UNFINISHED_STATUS,
@@ -19,11 +19,11 @@ import {
ALERT_DANGER_IMAGE,
MISSING_OR_DELETED_IMAGE_BREADCRUMB,
ROOT_IMAGE_TEXT,
-} 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';
+} from '~/packages_and_registries/container_registry/explorer/constants';
+import deleteContainerRepositoryTagsMutation from '~/packages_and_registries/container_registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql';
+import getContainerRepositoryDetailsQuery from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql';
-import component from '~/registry/explorer/pages/details.vue';
+import component from '~/packages_and_registries/container_registry/explorer/pages/details.vue';
import Tracking from '~/tracking';
import {
diff --git a/spec/frontend/registry/explorer/pages/index_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/index_spec.js
index b5f718b3e61..5f4cb8969bc 100644
--- a/spec/frontend/registry/explorer/pages/index_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/index_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import component from '~/registry/explorer/pages/index.vue';
+import component from '~/packages_and_registries/container_registry/explorer/pages/index.vue';
describe('List Page', () => {
let wrapper;
diff --git a/spec/frontend/registry/explorer/pages/list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js
index e1f24a2b65b..051d1e2a169 100644
--- a/spec/frontend/registry/explorer/pages/list_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js
@@ -7,25 +7,25 @@ import waitForPromises from 'helpers/wait_for_promises';
import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql';
import CleanupPolicyEnabledAlert from '~/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue';
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
-import DeleteImage from '~/registry/explorer/components/delete_image.vue';
-import CliCommands from '~/registry/explorer/components/list_page/cli_commands.vue';
-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 DeleteImage from '~/packages_and_registries/container_registry/explorer/components/delete_image.vue';
+import CliCommands from '~/packages_and_registries/container_registry/explorer/components/list_page/cli_commands.vue';
+import GroupEmptyState from '~/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state.vue';
+import ImageList from '~/packages_and_registries/container_registry/explorer/components/list_page/image_list.vue';
+import ProjectEmptyState from '~/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state.vue';
+import RegistryHeader from '~/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue';
import {
DELETE_IMAGE_SUCCESS_MESSAGE,
DELETE_IMAGE_ERROR_MESSAGE,
SORT_FIELDS,
-} from '~/registry/explorer/constants';
-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';
+} from '~/packages_and_registries/container_registry/explorer/constants';
+import deleteContainerRepositoryMutation from '~/packages_and_registries/container_registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql';
+import getContainerRepositoriesDetails from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repositories_details.query.graphql';
+import component from '~/packages_and_registries/container_registry/explorer/pages/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 { $toast } from 'jest/packages_and_registries/shared/mocks';
import {
graphQLImageListMock,
graphQLImageDeleteMock,
diff --git a/spec/frontend/registry/explorer/stubs.js b/spec/frontend/packages_and_registries/container_registry/explorer/stubs.js
index 4f65e73d3fa..7d281a53a59 100644
--- a/spec/frontend/registry/explorer/stubs.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/stubs.js
@@ -6,7 +6,7 @@ import {
} from '@gitlab/ui';
import { RouterLinkStub } from '@vue/test-utils';
import { stubComponent } from 'helpers/stub_component';
-import RealDeleteModal from '~/registry/explorer/components/details_page/delete_modal.vue';
+import RealDeleteModal from '~/packages_and_registries/container_registry/explorer/components/details_page/delete_modal.vue';
import RealListItem from '~/vue_shared/components/registry/list_item.vue';
export const GlModal = stubComponent(RealGlModal, {
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
index 1f0252965b0..625f00a8666 100644
--- a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
+++ b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
@@ -1,32 +1,40 @@
-import { GlFormInputGroup, GlFormGroup, GlSkeletonLoader, GlSprintf } from '@gitlab/ui';
+import {
+ GlFormInputGroup,
+ GlFormGroup,
+ GlSkeletonLoader,
+ GlSprintf,
+ GlEmptyState,
+} from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { stripTypenames } from 'helpers/graphql_helpers';
import waitForPromises from 'helpers/wait_for_promises';
+import { GRAPHQL_PAGE_SIZE } from '~/packages_and_registries/dependency_proxy/constants';
import DependencyProxyApp from '~/packages_and_registries/dependency_proxy/app.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import ManifestsList from '~/packages_and_registries/dependency_proxy/components/manifests_list.vue';
import getDependencyProxyDetailsQuery from '~/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql';
-import { proxyDetailsQuery, proxyData } from './mock_data';
+import { proxyDetailsQuery, proxyData, pagination, proxyManifests } from './mock_data';
const localVue = createLocalVue();
describe('DependencyProxyApp', () => {
let wrapper;
let apolloProvider;
+ let resolver;
const provideDefaults = {
groupPath: 'gitlab-org',
dependencyProxyAvailable: true,
+ noManifestsIllustration: 'noManifestsIllustration',
};
- function createComponent({
- provide = provideDefaults,
- resolver = jest.fn().mockResolvedValue(proxyDetailsQuery()),
- } = {}) {
+ function createComponent({ provide = provideDefaults } = {}) {
localVue.use(VueApollo);
const requestHandlers = [[getDependencyProxyDetailsQuery, resolver]];
@@ -53,6 +61,12 @@ describe('DependencyProxyApp', () => {
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findMainArea = () => wrapper.findByTestId('main-area');
const findProxyCountText = () => wrapper.findByTestId('proxy-count');
+ const findManifestList = () => wrapper.findComponent(ManifestsList);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+
+ beforeEach(() => {
+ resolver = jest.fn().mockResolvedValue(proxyDetailsQuery());
+ });
afterEach(() => {
wrapper.destroy();
@@ -78,8 +92,8 @@ describe('DependencyProxyApp', () => {
});
it('does not call the graphql endpoint', async () => {
- const resolver = jest.fn().mockResolvedValue(proxyDetailsQuery());
- createComponent({ ...createComponentArguments, resolver });
+ resolver = jest.fn().mockResolvedValue(proxyDetailsQuery());
+ createComponent({ ...createComponentArguments });
await waitForPromises();
@@ -145,14 +159,73 @@ describe('DependencyProxyApp', () => {
it('from group has a description with proxy count', () => {
expect(findProxyCountText().text()).toBe('Contains 2 blobs of images (1024 Bytes)');
});
+
+ describe('manifest lists', () => {
+ describe('when there are no manifests', () => {
+ beforeEach(() => {
+ resolver = jest.fn().mockResolvedValue(
+ proxyDetailsQuery({
+ extend: { dependencyProxyManifests: { nodes: [], pageInfo: pagination() } },
+ }),
+ );
+ createComponent();
+ return waitForPromises();
+ });
+
+ it('shows the empty state message', () => {
+ expect(findEmptyState().props()).toMatchObject({
+ svgPath: provideDefaults.noManifestsIllustration,
+ title: DependencyProxyApp.i18n.noManifestTitle,
+ });
+ });
+
+ it('hides the list', () => {
+ expect(findManifestList().exists()).toBe(false);
+ });
+ });
+
+ describe('when there are manifests', () => {
+ it('hides the empty state message', () => {
+ expect(findEmptyState().exists()).toBe(false);
+ });
+
+ it('shows list', () => {
+ expect(findManifestList().props()).toMatchObject({
+ manifests: proxyManifests(),
+ pagination: stripTypenames(pagination()),
+ });
+ });
+
+ it('prev-page event on list fetches the previous page', () => {
+ findManifestList().vm.$emit('prev-page');
+
+ expect(resolver).toHaveBeenCalledWith({
+ before: pagination().startCursor,
+ first: null,
+ fullPath: provideDefaults.groupPath,
+ last: GRAPHQL_PAGE_SIZE,
+ });
+ });
+
+ it('next-page event on list fetches the next page', () => {
+ findManifestList().vm.$emit('next-page');
+
+ expect(resolver).toHaveBeenCalledWith({
+ after: pagination().endCursor,
+ first: GRAPHQL_PAGE_SIZE,
+ fullPath: provideDefaults.groupPath,
+ });
+ });
+ });
+ });
});
+
describe('when the dependency proxy is disabled', () => {
beforeEach(() => {
- createComponent({
- resolver: jest
- .fn()
- .mockResolvedValue(proxyDetailsQuery({ extendSettings: { enabled: false } })),
- });
+ resolver = jest
+ .fn()
+ .mockResolvedValue(proxyDetailsQuery({ extendSettings: { enabled: false } }));
+ createComponent();
return waitForPromises();
});
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js
new file mode 100644
index 00000000000..9e4c747a1bd
--- /dev/null
+++ b/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js
@@ -0,0 +1,84 @@
+import { GlKeysetPagination } from '@gitlab/ui';
+import { stripTypenames } from 'helpers/graphql_helpers';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ManifestRow from '~/packages_and_registries/dependency_proxy/components/manifest_row.vue';
+
+import Component from '~/packages_and_registries/dependency_proxy/components/manifests_list.vue';
+import {
+ proxyManifests,
+ pagination,
+} from 'jest/packages_and_registries/dependency_proxy/mock_data';
+
+describe('Manifests List', () => {
+ let wrapper;
+
+ const defaultProps = {
+ manifests: proxyManifests(),
+ pagination: stripTypenames(pagination()),
+ };
+
+ const createComponent = (propsData = defaultProps) => {
+ wrapper = shallowMountExtended(Component, {
+ propsData,
+ });
+ };
+
+ const findRows = () => wrapper.findAllComponents(ManifestRow);
+ const findPagination = () => wrapper.findComponent(GlKeysetPagination);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('has the correct title', () => {
+ createComponent();
+
+ expect(wrapper.text()).toContain(Component.i18n.listTitle);
+ });
+
+ it('shows a row for every manifest', () => {
+ createComponent();
+
+ expect(findRows().length).toBe(defaultProps.manifests.length);
+ });
+
+ it('binds a manifest to each row', () => {
+ createComponent();
+
+ expect(findRows().at(0).props()).toMatchObject({
+ manifest: defaultProps.manifests[0],
+ });
+ });
+
+ describe('pagination', () => {
+ it('is hidden when there is no next or prev pages', () => {
+ createComponent({ ...defaultProps, pagination: {} });
+
+ expect(findPagination().exists()).toBe(false);
+ });
+
+ it('has the correct props', () => {
+ createComponent();
+
+ expect(findPagination().props()).toMatchObject({
+ ...defaultProps.pagination,
+ });
+ });
+
+ it('emits the next-page event', () => {
+ createComponent();
+
+ findPagination().vm.$emit('next');
+
+ expect(wrapper.emitted('next-page')).toEqual([[]]);
+ });
+
+ it('emits the prev-page event', () => {
+ createComponent();
+
+ findPagination().vm.$emit('prev');
+
+ expect(wrapper.emitted('prev-page')).toEqual([[]]);
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_row_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_row_spec.js
new file mode 100644
index 00000000000..b7cbd875497
--- /dev/null
+++ b/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_row_spec.js
@@ -0,0 +1,59 @@
+import { GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ListItem from '~/vue_shared/components/registry/list_item.vue';
+import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import Component from '~/packages_and_registries/dependency_proxy/components/manifest_row.vue';
+import { proxyManifests } from 'jest/packages_and_registries/dependency_proxy/mock_data';
+
+describe('Manifest Row', () => {
+ let wrapper;
+
+ const defaultProps = {
+ manifest: proxyManifests()[0],
+ };
+
+ const createComponent = (propsData = defaultProps) => {
+ wrapper = shallowMountExtended(Component, {
+ propsData,
+ stubs: {
+ GlSprintf,
+ TimeagoTooltip,
+ ListItem,
+ },
+ });
+ };
+
+ const findListItem = () => wrapper.findComponent(ListItem);
+ const findCachedMessages = () => wrapper.findByTestId('cached-message');
+ const findTimeAgoTooltip = () => wrapper.findComponent(TimeagoTooltip);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('has a list item', () => {
+ expect(findListItem().exists()).toBe(true);
+ });
+
+ it('displays the name', () => {
+ expect(wrapper.text()).toContain('alpine');
+ });
+
+ it('displays the version', () => {
+ expect(wrapper.text()).toContain('latest');
+ });
+
+ it('displays the cached time', () => {
+ expect(findCachedMessages().text()).toContain('Cached');
+ });
+
+ it('has a time ago tooltip component', () => {
+ expect(findTimeAgoTooltip().props()).toMatchObject({
+ time: defaultProps.manifest.createdAt,
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/mock_data.js b/spec/frontend/packages_and_registries/dependency_proxy/mock_data.js
index 23d42e109f9..8bad22b5287 100644
--- a/spec/frontend/packages_and_registries/dependency_proxy/mock_data.js
+++ b/spec/frontend/packages_and_registries/dependency_proxy/mock_data.js
@@ -7,7 +7,21 @@ export const proxyData = () => ({
export const proxySettings = (extend = {}) => ({ enabled: true, ...extend });
-export const proxyDetailsQuery = ({ extendSettings = {} } = {}) => ({
+export const proxyManifests = () => [
+ { createdAt: '2021-09-22T09:45:28Z', imageName: 'alpine:latest' },
+ { createdAt: '2021-09-21T09:45:28Z', imageName: 'alpine:stable' },
+];
+
+export const pagination = (extend) => ({
+ endCursor: 'eyJpZCI6IjIwNSIsIm5hbWUiOiJteS9jb21wYW55L2FwcC9teS1hcHAifQ',
+ hasNextPage: true,
+ hasPreviousPage: true,
+ startCursor: 'eyJpZCI6IjI0NyIsIm5hbWUiOiJ2ZXJzaW9uX3Rlc3QxIn0',
+ __typename: 'PageInfo',
+ ...extend,
+});
+
+export const proxyDetailsQuery = ({ extendSettings = {}, extend } = {}) => ({
data: {
group: {
...proxyData(),
@@ -16,6 +30,11 @@ export const proxyDetailsQuery = ({ extendSettings = {} } = {}) => ({
...proxySettings(extendSettings),
__typename: 'DependencyProxySetting',
},
+ dependencyProxyManifests: {
+ nodes: proxyManifests(),
+ pageInfo: pagination(),
+ },
+ ...extend,
},
},
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap
index 451cf743e35..519014bb9cf 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap
@@ -19,15 +19,15 @@ exports[`PackageTitle renders with tags 1`] = `
<div
class="gl-display-flex gl-flex-direction-column"
>
- <h1
- class="gl-font-size-h1 gl-mt-3 gl-mb-2"
+ <h2
+ class="gl-font-size-h1 gl-mt-3 gl-mb-0"
data-testid="title"
>
@gitlab-org/package-15
- </h1>
+ </h2>
<div
- class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1"
+ class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-3"
>
<gl-icon-stub
class="gl-mr-3"
@@ -117,15 +117,15 @@ exports[`PackageTitle renders without tags 1`] = `
<div
class="gl-display-flex gl-flex-direction-column"
>
- <h1
- class="gl-font-size-h1 gl-mt-3 gl-mb-2"
+ <h2
+ class="gl-font-size-h1 gl-mt-3 gl-mb-0"
data-testid="title"
>
@gitlab-org/package-15
- </h1>
+ </h2>
<div
- class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1"
+ class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-3"
>
<gl-icon-stub
class="gl-mr-3"
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/version_row_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/version_row_spec.js.snap
index 8f69f943112..c95538546c1 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/version_row_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/version_row_spec.js.snap
@@ -27,6 +27,7 @@ exports[`VersionRow renders 1`] = `
>
<span
class="gl-truncate"
+ data-testid="truncate-end-container"
title="@gitlab-org/package-15"
>
<span
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js
index 5119512564f..0bea84693f6 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js
@@ -16,16 +16,15 @@ import PackageFiles from '~/packages_and_registries/package_registry/components/
import PackageHistory from '~/packages_and_registries/package_registry/components/details/package_history.vue';
import PackageTitle from '~/packages_and_registries/package_registry/components/details/package_title.vue';
import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue';
+import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue';
import {
FETCH_PACKAGE_DETAILS_ERROR_MESSAGE,
- DELETE_PACKAGE_ERROR_MESSAGE,
PACKAGE_TYPE_COMPOSER,
DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
DELETE_PACKAGE_FILE_ERROR_MESSAGE,
PACKAGE_TYPE_NUGET,
} from '~/packages_and_registries/package_registry/constants';
-import destroyPackageMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package.mutation.graphql';
import destroyPackageFileMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_file.mutation.graphql';
import getPackageDetails from '~/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql';
import {
@@ -34,8 +33,6 @@ import {
packageVersions,
dependencyLinks,
emptyPackageDetailsQuery,
- packageDestroyMutation,
- packageDestroyMutationError,
packageFiles,
packageDestroyFileMutation,
packageDestroyFileMutationError,
@@ -64,14 +61,12 @@ describe('PackagesApp', () => {
function createComponent({
resolver = jest.fn().mockResolvedValue(packageDetailsQuery()),
- mutationResolver = jest.fn().mockResolvedValue(packageDestroyMutation()),
fileDeleteMutationResolver = jest.fn().mockResolvedValue(packageDestroyFileMutation()),
} = {}) {
localVue.use(VueApollo);
const requestHandlers = [
[getPackageDetails, resolver],
- [destroyPackageMutation, mutationResolver],
[destroyPackageFileMutation, fileDeleteMutationResolver],
];
apolloProvider = createMockApollo(requestHandlers);
@@ -82,6 +77,7 @@ describe('PackagesApp', () => {
provide,
stubs: {
PackageTitle,
+ DeletePackage,
GlModal: {
template: '<div></div>',
methods: {
@@ -108,6 +104,7 @@ describe('PackagesApp', () => {
const findDependenciesCountBadge = () => wrapper.findComponent(GlBadge);
const findNoDependenciesMessage = () => wrapper.findByTestId('no-dependencies-message');
const findDependencyRows = () => wrapper.findAllComponents(DependencyRow);
+ const findDeletePackage = () => wrapper.findComponent(DeletePackage);
afterEach(() => {
wrapper.destroy();
@@ -187,14 +184,6 @@ describe('PackagesApp', () => {
});
};
- const performDeletePackage = async () => {
- await findDeleteButton().trigger('click');
-
- findDeleteModal().vm.$emit('primary');
-
- await waitForPromises();
- };
-
afterEach(() => {
Object.defineProperty(document, 'referrer', {
value: originalReferrer,
@@ -220,7 +209,7 @@ describe('PackagesApp', () => {
await waitForPromises();
- await performDeletePackage();
+ findDeletePackage().vm.$emit('end');
expect(window.location.replace).toHaveBeenCalledWith(
'projectListUrl?showSuccessDeleteAlert=true',
@@ -234,45 +223,13 @@ describe('PackagesApp', () => {
await waitForPromises();
- await performDeletePackage();
+ findDeletePackage().vm.$emit('end');
expect(window.location.replace).toHaveBeenCalledWith(
'groupListUrl?showSuccessDeleteAlert=true',
);
});
});
-
- describe('request failure', () => {
- it('on global failure it displays an alert', async () => {
- createComponent({ mutationResolver: jest.fn().mockRejectedValue() });
-
- await waitForPromises();
-
- await performDeletePackage();
-
- expect(createFlash).toHaveBeenCalledWith(
- expect.objectContaining({
- message: DELETE_PACKAGE_ERROR_MESSAGE,
- }),
- );
- });
-
- it('on payload with error it displays an alert', async () => {
- createComponent({
- mutationResolver: jest.fn().mockResolvedValue(packageDestroyMutationError()),
- });
-
- await waitForPromises();
-
- await performDeletePackage();
-
- expect(createFlash).toHaveBeenCalledWith(
- expect.objectContaining({
- message: DELETE_PACKAGE_ERROR_MESSAGE,
- }),
- );
- });
- });
});
describe('package files', () => {
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/installations_commands_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/installations_commands_spec.js
index b24946c8638..8bb05b00e65 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/installations_commands_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/installations_commands_spec.js
@@ -33,12 +33,12 @@ describe('InstallationCommands', () => {
});
}
- const npmInstallation = () => wrapper.find(NpmInstallation);
- const mavenInstallation = () => wrapper.find(MavenInstallation);
- const conanInstallation = () => wrapper.find(ConanInstallation);
- const nugetInstallation = () => wrapper.find(NugetInstallation);
- const pypiInstallation = () => wrapper.find(PypiInstallation);
- const composerInstallation = () => wrapper.find(ComposerInstallation);
+ const npmInstallation = () => wrapper.findComponent(NpmInstallation);
+ const mavenInstallation = () => wrapper.findComponent(MavenInstallation);
+ const conanInstallation = () => wrapper.findComponent(ConanInstallation);
+ const nugetInstallation = () => wrapper.findComponent(NugetInstallation);
+ const pypiInstallation = () => wrapper.findComponent(PypiInstallation);
+ const composerInstallation = () => wrapper.findComponent(ComposerInstallation);
afterEach(() => {
wrapper.destroy();
@@ -57,7 +57,7 @@ describe('InstallationCommands', () => {
it(`${packageEntity.packageType} instructions exist`, () => {
createComponent({ packageEntity });
- expect(selector()).toExist();
+ expect(selector().exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/functional/delete_package_spec.js b/spec/frontend/packages_and_registries/package_registry/components/functional/delete_package_spec.js
new file mode 100644
index 00000000000..5de30829fa5
--- /dev/null
+++ b/spec/frontend/packages_and_registries/package_registry/components/functional/delete_package_spec.js
@@ -0,0 +1,160 @@
+import { createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import waitForPromises from 'helpers/wait_for_promises';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import createFlash from '~/flash';
+import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue';
+
+import destroyPackageMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package.mutation.graphql';
+import getPackagesQuery from '~/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql';
+import {
+ packageDestroyMutation,
+ packageDestroyMutationError,
+ packagesListQuery,
+} from '../../mock_data';
+
+jest.mock('~/flash');
+
+const localVue = createLocalVue();
+
+describe('DeletePackage', () => {
+ let wrapper;
+ let apolloProvider;
+ let resolver;
+ let mutationResolver;
+
+ const eventPayload = { id: '1' };
+
+ function createComponent(propsData = {}) {
+ localVue.use(VueApollo);
+
+ const requestHandlers = [
+ [getPackagesQuery, resolver],
+ [destroyPackageMutation, mutationResolver],
+ ];
+ apolloProvider = createMockApollo(requestHandlers);
+
+ wrapper = shallowMountExtended(DeletePackage, {
+ propsData,
+ localVue,
+ apolloProvider,
+ scopedSlots: {
+ default(props) {
+ return this.$createElement('button', {
+ attrs: {
+ 'data-testid': 'trigger-button',
+ },
+ on: {
+ click: props.deletePackage,
+ },
+ });
+ },
+ },
+ });
+ }
+
+ const findButton = () => wrapper.findByTestId('trigger-button');
+
+ const clickOnButtonAndWait = (payload) => {
+ findButton().trigger('click', payload);
+ return waitForPromises();
+ };
+
+ beforeEach(() => {
+ resolver = jest.fn().mockResolvedValue(packagesListQuery());
+ mutationResolver = jest.fn().mockResolvedValue(packageDestroyMutation());
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('binds deletePackage method to the default slot', () => {
+ createComponent();
+
+ findButton().trigger('click');
+
+ expect(wrapper.emitted('start')).toEqual([[]]);
+ });
+
+ it('calls apollo mutation', async () => {
+ createComponent();
+
+ await clickOnButtonAndWait(eventPayload);
+
+ expect(mutationResolver).toHaveBeenCalledWith(eventPayload);
+ });
+
+ it('passes refetchQueries to apollo mutate', async () => {
+ const variables = { isGroupPage: true };
+ createComponent({
+ refetchQueries: [{ query: getPackagesQuery, variables }],
+ });
+
+ await clickOnButtonAndWait(eventPayload);
+
+ expect(mutationResolver).toHaveBeenCalledWith(eventPayload);
+ expect(resolver).toHaveBeenCalledWith(variables);
+ });
+
+ describe('on mutation success', () => {
+ it('emits end event', async () => {
+ createComponent();
+
+ await clickOnButtonAndWait(eventPayload);
+
+ expect(wrapper.emitted('end')).toEqual([[]]);
+ });
+
+ it('does not call createFlash', async () => {
+ createComponent();
+
+ await clickOnButtonAndWait(eventPayload);
+
+ expect(createFlash).not.toHaveBeenCalled();
+ });
+
+ it('calls createFlash with the success message when showSuccessAlert is true', async () => {
+ createComponent({ showSuccessAlert: true });
+
+ await clickOnButtonAndWait(eventPayload);
+
+ expect(createFlash).toHaveBeenCalledWith({
+ message: DeletePackage.i18n.successMessage,
+ type: 'success',
+ });
+ });
+ });
+
+ describe.each`
+ errorType | mutationResolverResponse
+ ${'connectionError'} | ${jest.fn().mockRejectedValue()}
+ ${'localError'} | ${jest.fn().mockResolvedValue(packageDestroyMutationError())}
+ `('on mutation $errorType', ({ mutationResolverResponse }) => {
+ beforeEach(() => {
+ mutationResolver = mutationResolverResponse;
+ });
+
+ it('emits end event', async () => {
+ createComponent();
+
+ await clickOnButtonAndWait(eventPayload);
+
+ expect(wrapper.emitted('end')).toEqual([[]]);
+ });
+
+ it('calls createFlash with the error message', async () => {
+ createComponent({ showSuccessAlert: true });
+
+ await clickOnButtonAndWait(eventPayload);
+
+ expect(createFlash).toHaveBeenCalledWith({
+ message: DeletePackage.i18n.errorMessage,
+ type: 'warning',
+ captureError: true,
+ error: expect.any(Error),
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/app_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/app_spec.js.snap
index 1b556be5873..5af75868084 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/app_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/app_spec.js.snap
@@ -8,5 +8,62 @@ exports[`PackagesListApp renders 1`] = `
/>
<package-search-stub />
+
+ <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"
+ role="img"
+ src="emptyListIllustration"
+ />
+ </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="emptyListHelpUrl"
+ routertag="a"
+ target="_blank"
+ >
+ publish and share your packages
+ </b-link-stub>
+ with GitLab.
+ </p>
+
+ <div
+ class="gl-display-flex gl-flex-wrap gl-justify-content-center"
+ >
+ <!---->
+
+ <!---->
+ </div>
+ </div>
+ </div>
+ </section>
+ </div>
</div>
`;
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/app_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/app_spec.js
index 3958cdf21bb..ad848f367e0 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/app_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/app_spec.js
@@ -2,22 +2,25 @@ import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
+import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import PackageListApp from '~/packages_and_registries/package_registry/components/list/app.vue';
import PackageTitle from '~/packages_and_registries/package_registry/components/list/package_title.vue';
import PackageSearch from '~/packages_and_registries/package_registry/components/list/package_search.vue';
+import OriginalPackageList from '~/packages_and_registries/package_registry/components/list/packages_list.vue';
+import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue';
import {
PROJECT_RESOURCE_TYPE,
GROUP_RESOURCE_TYPE,
- LIST_QUERY_DEBOUNCE_TIME,
+ GRAPHQL_PAGE_SIZE,
} from '~/packages_and_registries/package_registry/constants';
import getPackagesQuery from '~/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql';
-import { packagesListQuery } from '../../mock_data';
+import { packagesListQuery, packageData, pagination } from '../../mock_data';
jest.mock('~/lib/utils/common_utils');
jest.mock('~/flash');
@@ -39,11 +42,20 @@ describe('PackagesListApp', () => {
const PackageList = {
name: 'package-list',
template: '<div><slot name="empty-state"></slot></div>',
+ props: OriginalPackageList.props,
};
const GlLoadingIcon = { name: 'gl-loading-icon', template: '<div>loading</div>' };
+ const searchPayload = {
+ sort: 'VERSION_DESC',
+ filters: { packageName: 'foo', packageType: 'CONAN' },
+ };
+
const findPackageTitle = () => wrapper.findComponent(PackageTitle);
const findSearch = () => wrapper.findComponent(PackageSearch);
+ const findListComponent = () => wrapper.findComponent(PackageList);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findDeletePackage = () => wrapper.findComponent(DeletePackage);
const mountComponent = ({
resolver = jest.fn().mockResolvedValue(packagesListQuery()),
@@ -61,9 +73,10 @@ describe('PackagesListApp', () => {
stubs: {
GlEmptyState,
GlLoadingIcon,
- PackageList,
GlSprintf,
GlLink,
+ PackageList,
+ DeletePackage,
},
});
};
@@ -72,15 +85,24 @@ describe('PackagesListApp', () => {
wrapper.destroy();
});
- const waitForDebouncedApollo = () => {
- jest.advanceTimersByTime(LIST_QUERY_DEBOUNCE_TIME);
+ const waitForFirstRequest = () => {
+ // emit a search update so the query is executed
+ findSearch().vm.$emit('update', { sort: 'NAME_DESC', filters: [] });
return waitForPromises();
};
+ it('does not execute the query without sort being set', () => {
+ const resolver = jest.fn().mockResolvedValue(packagesListQuery());
+
+ mountComponent({ resolver });
+
+ expect(resolver).not.toHaveBeenCalled();
+ });
+
it('renders', async () => {
mountComponent();
- await waitForDebouncedApollo();
+ await waitForFirstRequest();
expect(wrapper.element).toMatchSnapshot();
});
@@ -88,7 +110,7 @@ describe('PackagesListApp', () => {
it('has a package title', async () => {
mountComponent();
- await waitForDebouncedApollo();
+ await waitForFirstRequest();
expect(findPackageTitle().exists()).toBe(true);
expect(findPackageTitle().props('count')).toBe(2);
@@ -105,25 +127,54 @@ describe('PackagesListApp', () => {
const resolver = jest.fn().mockResolvedValue(packagesListQuery());
mountComponent({ resolver });
- const payload = {
- sort: 'VERSION_DESC',
- filters: { packageName: 'foo', packageType: 'CONAN' },
- };
-
- findSearch().vm.$emit('update', payload);
+ findSearch().vm.$emit('update', searchPayload);
- await waitForDebouncedApollo();
- jest.advanceTimersByTime(LIST_QUERY_DEBOUNCE_TIME);
+ await waitForPromises();
expect(resolver).toHaveBeenCalledWith(
expect.objectContaining({
- groupSort: payload.sort,
- ...payload.filters,
+ groupSort: searchPayload.sort,
+ ...searchPayload.filters,
}),
);
});
});
+ describe('list component', () => {
+ let resolver;
+
+ beforeEach(() => {
+ resolver = jest.fn().mockResolvedValue(packagesListQuery());
+ mountComponent({ resolver });
+
+ return waitForFirstRequest();
+ });
+
+ it('exists and has the right props', () => {
+ expect(findListComponent().props()).toMatchObject({
+ list: expect.arrayContaining([expect.objectContaining({ id: packageData().id })]),
+ isLoading: false,
+ pageInfo: expect.objectContaining({ endCursor: pagination().endCursor }),
+ });
+ });
+
+ it('when list emits next-page fetches the next set of records', () => {
+ findListComponent().vm.$emit('next-page');
+
+ expect(resolver).toHaveBeenCalledWith(
+ expect.objectContaining({ after: pagination().endCursor, first: GRAPHQL_PAGE_SIZE }),
+ );
+ });
+
+ it('when list emits prev-page fetches the prev set of records', () => {
+ findListComponent().vm.$emit('prev-page');
+
+ expect(resolver).toHaveBeenCalledWith(
+ expect.objectContaining({ before: pagination().startCursor, last: GRAPHQL_PAGE_SIZE }),
+ );
+ });
+ });
+
describe.each`
type | sortType
${PROJECT_RESOURCE_TYPE} | ${'sort'}
@@ -136,9 +187,9 @@ describe('PackagesListApp', () => {
beforeEach(() => {
provide = { ...defaultProvide, isGroupPage };
- resolver = jest.fn().mockResolvedValue(packagesListQuery(type));
+ resolver = jest.fn().mockResolvedValue(packagesListQuery({ type }));
mountComponent({ provide, resolver });
- return waitForDebouncedApollo();
+ return waitForFirstRequest();
});
it('succeeds', () => {
@@ -147,8 +198,85 @@ describe('PackagesListApp', () => {
it('calls the resolver with the right parameters', () => {
expect(resolver).toHaveBeenCalledWith(
- expect.objectContaining({ isGroupPage, [sortType]: '' }),
+ expect.objectContaining({ isGroupPage, [sortType]: 'NAME_DESC' }),
);
});
});
+
+ describe('empty state', () => {
+ beforeEach(() => {
+ const resolver = jest.fn().mockResolvedValue(packagesListQuery({ extend: { nodes: [] } }));
+ mountComponent({ resolver });
+
+ return waitForFirstRequest();
+ });
+ it('generate the correct empty list link', () => {
+ const link = findListComponent().findComponent(GlLink);
+
+ expect(link.attributes('href')).toBe(defaultProvide.emptyListHelpUrl);
+ expect(link.text()).toBe('publish and share your packages');
+ });
+
+ it('includes the right content on the default tab', () => {
+ expect(findEmptyState().text()).toContain(PackageListApp.i18n.emptyPageTitle);
+ });
+ });
+
+ describe('filter without results', () => {
+ beforeEach(async () => {
+ mountComponent();
+
+ await waitForFirstRequest();
+
+ findSearch().vm.$emit('update', searchPayload);
+
+ return nextTick();
+ });
+
+ it('should show specific empty message', () => {
+ expect(findEmptyState().text()).toContain(PackageListApp.i18n.noResultsTitle);
+ expect(findEmptyState().text()).toContain(PackageListApp.i18n.widenFilters);
+ });
+ });
+
+ describe('delete package', () => {
+ it('exists and has the correct props', async () => {
+ mountComponent();
+
+ await waitForFirstRequest();
+
+ expect(findDeletePackage().props()).toMatchObject({
+ refetchQueries: [{ query: getPackagesQuery, variables: {} }],
+ showSuccessAlert: true,
+ });
+ });
+
+ it('deletePackage is bound to package-list package:delete event', async () => {
+ mountComponent();
+
+ await waitForFirstRequest();
+
+ findListComponent().vm.$emit('package:delete', { id: 1 });
+
+ expect(findDeletePackage().emitted('start')).toEqual([[]]);
+ });
+
+ it('start and end event set loading correctly', async () => {
+ mountComponent();
+
+ await waitForFirstRequest();
+
+ findDeletePackage().vm.$emit('start');
+
+ await nextTick();
+
+ expect(findListComponent().props('isLoading')).toBe(true);
+
+ findDeletePackage().vm.$emit('end');
+
+ await nextTick();
+
+ expect(findListComponent().props('isLoading')).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
index b624e66482d..de4e9c8ae5b 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
@@ -1,93 +1,86 @@
-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 { packageList } from 'jest/packages/mock_data';
+import { GlKeysetPagination, GlModal, GlSprintf } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import PackagesListRow 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 {
+ DELETE_PACKAGE_TRACKING_ACTION,
+ REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
+ CANCEL_DELETE_PACKAGE_TRACKING_ACTION,
+} from '~/packages_and_registries/package_registry/constants';
import PackagesList from '~/packages_and_registries/package_registry/components/list/packages_list.vue';
import Tracking from '~/tracking';
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
+import { packageData } from '../../mock_data';
describe('packages_list', () => {
let wrapper;
- let store;
+
+ const firstPackage = packageData();
+ const secondPackage = {
+ ...packageData(),
+ id: 'gid://gitlab/Packages::Package/112',
+ name: 'second-package',
+ };
+
+ const defaultProps = {
+ list: [firstPackage, secondPackage],
+ isLoading: false,
+ pageInfo: {},
+ };
const EmptySlotStub = { name: 'empty-slot-stub', template: '<div>bar</div>' };
+ const GlModalStub = {
+ name: GlModal.name,
+ template: '<div><slot></slot></div>',
+ methods: { show: jest.fn() },
+ };
- const findPackagesListLoader = () => wrapper.find(PackagesListLoader);
- const findPackageListPagination = () => wrapper.find(GlPagination);
- const findPackageListDeleteModal = () => wrapper.find(GlModal);
- const findEmptySlot = () => wrapper.find(EmptySlotStub);
- const findPackagesListRow = () => wrapper.find(PackagesListRow);
-
- const createStore = (isGroupPage, packages, isLoading) => {
- const state = {
- isLoading,
- packages,
- pagination: {
- perPage: 1,
- total: 1,
- page: 1,
- },
- config: {
- isGroupPage,
+ const findPackagesListLoader = () => wrapper.findComponent(PackagesListLoader);
+ const findPackageListPagination = () => wrapper.findComponent(GlKeysetPagination);
+ const findPackageListDeleteModal = () => wrapper.findComponent(GlModalStub);
+ const findEmptySlot = () => wrapper.findComponent(EmptySlotStub);
+ const findPackagesListRow = () => wrapper.findComponent(PackagesListRow);
+
+ const mountComponent = (props) => {
+ wrapper = shallowMountExtended(PackagesList, {
+ propsData: {
+ ...defaultProps,
+ ...props,
},
- sorting: {
- orderBy: 'version',
- sort: 'desc',
+ stubs: {
+ GlModal: GlModalStub,
+ GlSprintf,
},
- };
- store = new Vuex.Store({
- state,
- getters: {
- getList: () => packages,
+ slots: {
+ 'empty-state': EmptySlotStub,
},
});
- store.dispatch = jest.fn();
};
- const mountComponent = ({
- isGroupPage = false,
- packages = packageList,
- isLoading = false,
- ...options
- } = {}) => {
- createStore(isGroupPage, packages, isLoading);
-
- wrapper = mount(PackagesList, {
- localVue,
- store,
- stubs: {
- ...stubChildren(PackagesList),
- GlTable,
- GlModal,
- },
- ...options,
- });
- };
+ beforeEach(() => {
+ GlModalStub.methods.show.mockReset();
+ });
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
describe('when is loading', () => {
beforeEach(() => {
- mountComponent({
- packages: [],
- isLoading: true,
- });
+ mountComponent({ isLoading: true });
});
- it('shows skeleton loader when loading', () => {
+ it('shows skeleton loader', () => {
expect(findPackagesListLoader().exists()).toBe(true);
});
+
+ it('does not show the rows', () => {
+ expect(findPackagesListRow().exists()).toBe(false);
+ });
+
+ it('does not show the pagination', () => {
+ expect(findPackageListPagination().exists()).toBe(false);
+ });
});
describe('when is not loading', () => {
@@ -95,74 +88,61 @@ describe('packages_list', () => {
mountComponent();
});
- it('does not show skeleton loader when not loading', () => {
+ it('does not show skeleton loader', () => {
expect(findPackagesListLoader().exists()).toBe(false);
});
- });
- describe('layout', () => {
- beforeEach(() => {
- mountComponent();
+ it('shows the rows', () => {
+ expect(findPackagesListRow().exists()).toBe(true);
});
+ });
+ describe('layout', () => {
it('contains a pagination component', () => {
- const sorting = findPackageListPagination();
- expect(sorting.exists()).toBe(true);
+ mountComponent({ pageInfo: { hasPreviousPage: true } });
+
+ expect(findPackageListPagination().exists()).toBe(true);
});
it('contains a modal component', () => {
- const sorting = findPackageListDeleteModal();
- expect(sorting.exists()).toBe(true);
+ mountComponent();
+
+ expect(findPackageListDeleteModal().exists()).toBe(true);
});
});
describe('when the user can destroy the package', () => {
beforeEach(() => {
mountComponent();
+ findPackagesListRow().vm.$emit('packageToDelete', firstPackage);
+ return nextTick();
});
- it('setItemToBeDeleted sets itemToBeDeleted and open the modal', () => {
- const mockModalShow = jest.spyOn(wrapper.vm.$refs.packageListDeleteModal, 'show');
- const item = last(wrapper.vm.list);
+ it('deleting a package opens the modal', () => {
+ expect(findPackageListDeleteModal().text()).toContain(firstPackage.name);
+ });
- findPackagesListRow().vm.$emit('packageToDelete', item);
+ it('confirming on the modal emits package:delete', async () => {
+ findPackageListDeleteModal().vm.$emit('ok');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.itemToBeDeleted).toEqual(item);
- expect(mockModalShow).toHaveBeenCalled();
- });
- });
+ await nextTick();
- it('deleteItemConfirmation resets itemToBeDeleted', () => {
- wrapper.setData({ itemToBeDeleted: 1 });
- wrapper.vm.deleteItemConfirmation();
- expect(wrapper.vm.itemToBeDeleted).toEqual(null);
+ expect(wrapper.emitted('package:delete')[0]).toEqual([firstPackage]);
});
- it('deleteItemConfirmation emit package:delete', () => {
- const itemToBeDeleted = { id: 2 };
- wrapper.setData({ itemToBeDeleted });
- wrapper.vm.deleteItemConfirmation();
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.emitted('package:delete')[0]).toEqual([itemToBeDeleted]);
- });
- });
+ it('closing the modal resets itemToBeDeleted', async () => {
+ // triggering the v-model
+ findPackageListDeleteModal().vm.$emit('input', false);
- it('deleteItemCanceled resets itemToBeDeleted', () => {
- wrapper.setData({ itemToBeDeleted: 1 });
- wrapper.vm.deleteItemCanceled();
- expect(wrapper.vm.itemToBeDeleted).toEqual(null);
+ await nextTick();
+
+ expect(findPackageListDeleteModal().text()).not.toContain(firstPackage.name);
});
});
describe('when the list is empty', () => {
beforeEach(() => {
- mountComponent({
- packages: [],
- slots: {
- 'empty-state': EmptySlotStub,
- },
- });
+ mountComponent({ list: [] });
});
it('show the empty slot', () => {
@@ -171,45 +151,59 @@ describe('packages_list', () => {
});
});
- describe('pagination component', () => {
- let pagination;
- let modelEvent;
-
+ describe('pagination ', () => {
beforeEach(() => {
- mountComponent();
- pagination = findPackageListPagination();
- // retrieve the event used by v-model, a more sturdy approach than hardcoding it
- modelEvent = pagination.vm.$options.model.event;
+ mountComponent({ pageInfo: { hasPreviousPage: true } });
});
- it('emits page:changed events when the page changes', () => {
- pagination.vm.$emit(modelEvent, 2);
- expect(wrapper.emitted('page:changed')).toEqual([[2]]);
+ it('emits prev-page events when the prev event is fired', () => {
+ findPackageListPagination().vm.$emit('prev');
+
+ expect(wrapper.emitted('prev-page')).toEqual([[]]);
+ });
+
+ it('emits next-page events when the next event is fired', () => {
+ findPackageListPagination().vm.$emit('next');
+
+ expect(wrapper.emitted('next-page')).toEqual([[]]);
});
});
describe('tracking', () => {
let eventSpy;
- let utilSpy;
- const category = 'foo';
+ const category = 'UI::NpmPackages';
beforeEach(() => {
- mountComponent();
eventSpy = jest.spyOn(Tracking, 'event');
- utilSpy = jest.spyOn(SharedUtils, 'packageTypeToTrackCategory').mockReturnValue(category);
- wrapper.setData({ itemToBeDeleted: { package_type: 'conan' } });
+ mountComponent();
+ findPackagesListRow().vm.$emit('packageToDelete', firstPackage);
+ return nextTick();
});
- it('tracking category calls packageTypeToTrackCategory', () => {
- expect(wrapper.vm.tracking.category).toBe(category);
- expect(utilSpy).toHaveBeenCalledWith('conan');
+ it('requesting the delete tracks the right action', () => {
+ expect(eventSpy).toHaveBeenCalledWith(
+ category,
+ REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
+ expect.any(Object),
+ );
+ });
+
+ it('confirming delete tracks the right action', () => {
+ findPackageListDeleteModal().vm.$emit('ok');
+
+ expect(eventSpy).toHaveBeenCalledWith(
+ category,
+ DELETE_PACKAGE_TRACKING_ACTION,
+ expect.any(Object),
+ );
});
- it('deleteItemConfirmation calls event', () => {
- wrapper.vm.deleteItemConfirmation();
+ it('canceling delete tracks the right action', () => {
+ findPackageListDeleteModal().vm.$emit('cancel');
+
expect(eventSpy).toHaveBeenCalledWith(
category,
- TrackingActions.DELETE_PACKAGE,
+ CANCEL_DELETE_PACKAGE_TRACKING_ACTION,
expect.any(Object),
);
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js
index e65b2a6f320..bed7a07ff36 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js
@@ -1,6 +1,6 @@
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { sortableFields } from '~/packages/list/utils';
+import { sortableFields } from '~/packages_and_registries/package_registry/utils';
import component from '~/packages_and_registries/package_registry/components/list/package_search.vue';
import PackageTypeToken from '~/packages_and_registries/package_registry/components/list/tokens/package_type_token.vue';
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js
index 3fa96ce1d29..e992ba12faa 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js
@@ -1,5 +1,4 @@
import { shallowMount } from '@vue/test-utils';
-import { LIST_INTRO_TEXT, LIST_TITLE_TEXT } from '~/packages/list/constants';
import PackageTitle from '~/packages_and_registries/package_registry/components/list/package_title.vue';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
@@ -37,8 +36,8 @@ describe('PackageTitle', () => {
mountComponent();
expect(findTitleArea().props()).toMatchObject({
- title: LIST_TITLE_TEXT,
- infoMessages: [{ text: LIST_INTRO_TEXT, link: 'foo' }],
+ title: PackageTitle.i18n.LIST_TITLE_TEXT,
+ infoMessages: [{ text: PackageTitle.i18n.LIST_INTRO_TEXT, link: 'foo' }],
});
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js
index b0cbe34f0b9..26b2f3b359f 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js
@@ -1,7 +1,7 @@
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';
+import component from '~/packages_and_registries/package_registry/components/list/tokens/package_type_token.vue';
+import { PACKAGE_TYPES } from '~/packages_and_registries/package_registry/constants';
describe('packages_filter', () => {
let wrapper;
@@ -41,8 +41,8 @@ describe('packages_filter', () => {
(packageType, index) => {
mountComponent();
const item = findFilteredSearchSuggestions().at(index);
- expect(item.text()).toBe(packageType.title);
- expect(item.props('value')).toBe(packageType.type);
+ expect(item.text()).toBe(packageType);
+ expect(item.props('value')).toBe(packageType);
},
);
});
diff --git a/spec/frontend/packages_and_registries/package_registry/mock_data.js b/spec/frontend/packages_and_registries/package_registry/mock_data.js
index 70fc096fa44..bacc748db81 100644
--- a/spec/frontend/packages_and_registries/package_registry/mock_data.js
+++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js
@@ -1,3 +1,5 @@
+import capitalize from 'lodash/capitalize';
+
export const packageTags = () => [
{ id: 'gid://gitlab/Packages::Tag/87', name: 'bananas_9', __typename: 'PackageTag' },
{ id: 'gid://gitlab/Packages::Tag/86', name: 'bananas_8', __typename: 'PackageTag' },
@@ -156,6 +158,15 @@ export const nugetMetadata = () => ({
projectUrl: 'projectUrl',
});
+export const pagination = (extend) => ({
+ endCursor: 'eyJpZCI6IjIwNSIsIm5hbWUiOiJteS9jb21wYW55L2FwcC9teS1hcHAifQ',
+ hasNextPage: true,
+ hasPreviousPage: true,
+ startCursor: 'eyJpZCI6IjI0NyIsIm5hbWUiOiJ2ZXJzaW9uX3Rlc3QxIn0',
+ __typename: 'PageInfo',
+ ...extend,
+});
+
export const packageDetailsQuery = (extendPackage) => ({
data: {
package: {
@@ -256,7 +267,7 @@ export const packageDestroyFileMutationError = () => ({
],
});
-export const packagesListQuery = (type = 'group') => ({
+export const packagesListQuery = ({ type = 'group', extend = {}, extendPagination = {} } = {}) => ({
data: {
[type]: {
packages: {
@@ -277,9 +288,11 @@ export const packagesListQuery = (type = 'group') => ({
pipelines: { nodes: [] },
},
],
+ pageInfo: pagination(extendPagination),
__typename: 'PackageConnection',
},
- __typename: 'Group',
+ ...extend,
+ __typename: capitalize(type),
},
},
});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js
index c56244a9138..5c9ade7f785 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlFormGroup, GlFormSelect } from 'jest/registry/shared/stubs';
+import { GlFormGroup, GlFormSelect } from 'jest/packages_and_registries/shared/stubs';
import component from '~/packages_and_registries/settings/project/components/expiration_dropdown.vue';
describe('ExpirationDropdown', () => {
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_input_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_input_spec.js
index dd876d1d295..6b681924fcf 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_input_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_input_spec.js
@@ -1,6 +1,6 @@
import { GlSprintf, GlFormInput, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { GlFormGroup } from 'jest/registry/shared/stubs';
+import { GlFormGroup } from 'jest/packages_and_registries/shared/stubs';
import component from '~/packages_and_registries/settings/project/components/expiration_input.vue';
import { NAME_REGEX_LENGTH } from '~/packages_and_registries/settings/project/constants';
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_run_text_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_run_text_spec.js
index 854830391c5..94f7783afe7 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_run_text_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_run_text_spec.js
@@ -1,6 +1,6 @@
import { GlFormInput } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { GlFormGroup } from 'jest/registry/shared/stubs';
+import { GlFormGroup } from 'jest/packages_and_registries/shared/stubs';
import component from '~/packages_and_registries/settings/project/components/expiration_run_text.vue';
import {
NEXT_CLEANUP_LABEL,
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_toggle_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_toggle_spec.js
index 3a3eb089b43..45039614e49 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_toggle_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_toggle_spec.js
@@ -1,6 +1,6 @@
import { GlToggle, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { GlFormGroup } from 'jest/registry/shared/stubs';
+import { GlFormGroup } from 'jest/packages_and_registries/shared/stubs';
import component from '~/packages_and_registries/settings/project/components/expiration_toggle.vue';
import {
ENABLED_TOGGLE_DESCRIPTION,
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js
index 3a71af94d5a..bc104a25ef9 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js
@@ -2,7 +2,7 @@ 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 { GlCard, GlLoadingIcon } from 'jest/registry/shared/stubs';
+import { GlCard, GlLoadingIcon } from 'jest/packages_and_registries/shared/stubs';
import component from '~/packages_and_registries/settings/project/components/settings_form.vue';
import {
UPDATE_SETTINGS_ERROR_MESSAGE,
diff --git a/spec/frontend/registry/shared/mocks.js b/spec/frontend/packages_and_registries/shared/mocks.js
index fdef38b6f10..fdef38b6f10 100644
--- a/spec/frontend/registry/shared/mocks.js
+++ b/spec/frontend/packages_and_registries/shared/mocks.js
diff --git a/spec/frontend/registry/shared/stubs.js b/spec/frontend/packages_and_registries/shared/stubs.js
index ad41eb42df4..ad41eb42df4 100644
--- a/spec/frontend/registry/shared/stubs.js
+++ b/spec/frontend/packages_and_registries/shared/stubs.js
diff --git a/spec/frontend/pages/admin/projects/components/namespace_select_spec.js b/spec/frontend/pages/admin/projects/components/namespace_select_spec.js
index c579aa2f2da..1fcc00489e3 100644
--- a/spec/frontend/pages/admin/projects/components/namespace_select_spec.js
+++ b/spec/frontend/pages/admin/projects/components/namespace_select_spec.js
@@ -38,7 +38,7 @@ describe('Dropdown select component', () => {
it('creates a hidden input if fieldName is provided', () => {
mountDropdown({ fieldName: 'namespace-input' });
- expect(findNamespaceInput()).toExist();
+ expect(findNamespaceInput().exists()).toBe(true);
expect(findNamespaceInput().attributes('name')).toBe('namespace-input');
});
@@ -57,9 +57,9 @@ describe('Dropdown select component', () => {
// wait for dropdown options to populate
await wrapper.vm.$nextTick();
- expect(findDropdownOption('user: Administrator')).toExist();
- expect(findDropdownOption('group: GitLab Org')).toExist();
- expect(findDropdownOption('group: Foobar')).not.toExist();
+ expect(findDropdownOption('user: Administrator').exists()).toBe(true);
+ expect(findDropdownOption('group: GitLab Org').exists()).toBe(true);
+ expect(findDropdownOption('group: Foobar').exists()).toBe(false);
findDropdownOption('user: Administrator').trigger('click');
await wrapper.vm.$nextTick();
diff --git a/spec/frontend/pages/dashboard/todos/index/todos_spec.js b/spec/frontend/pages/dashboard/todos/index/todos_spec.js
index de8b29d54fc..5bba98bdf96 100644
--- a/spec/frontend/pages/dashboard/todos/index/todos_spec.js
+++ b/spec/frontend/pages/dashboard/todos/index/todos_spec.js
@@ -94,13 +94,13 @@ describe('Todos', () => {
});
it('updates pending text', () => {
- expect(document.querySelector('.todos-pending .badge').innerHTML).toEqual(
+ expect(document.querySelector('.js-todos-pending .badge').innerHTML).toEqual(
addDelimiter(TEST_COUNT_BIG),
);
});
it('updates done text', () => {
- expect(document.querySelector('.todos-done .badge').innerHTML).toEqual(
+ expect(document.querySelector('.js-todos-done .badge').innerHTML).toEqual(
addDelimiter(TEST_DONE_COUNT_BIG),
);
});
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap
index 3aa0e99a858..3e371a8765f 100644
--- a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap
+++ b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap
@@ -135,6 +135,7 @@ exports[`Learn GitLab renders correctly 1`] = `
<a
class="gl-link"
data-track-action="click_link"
+ data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Set up CI/CD"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
@@ -156,6 +157,7 @@ exports[`Learn GitLab renders correctly 1`] = `
<a
class="gl-link"
data-track-action="click_link"
+ data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Start a free Ultimate trial"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
@@ -177,6 +179,7 @@ exports[`Learn GitLab renders correctly 1`] = `
<a
class="gl-link"
data-track-action="click_link"
+ data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Add code owners"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
@@ -205,6 +208,7 @@ exports[`Learn GitLab renders correctly 1`] = `
<a
class="gl-link"
data-track-action="click_link"
+ data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Add merge request approval"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
@@ -269,6 +273,7 @@ exports[`Learn GitLab renders correctly 1`] = `
<a
class="gl-link"
data-track-action="click_link"
+ data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Create an issue"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
@@ -290,6 +295,7 @@ exports[`Learn GitLab renders correctly 1`] = `
<a
class="gl-link"
data-track-action="click_link"
+ data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Submit a merge request"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
@@ -347,6 +353,7 @@ exports[`Learn GitLab renders correctly 1`] = `
<a
class="gl-link"
data-track-action="click_link"
+ data-track-experiment="change_continuous_onboarding_link_urls"
data-track-label="Run a Security scan using CI/CD"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
href="http://example.com/"
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js
index f8099d7e95a..7e97a539a99 100644
--- a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js
+++ b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js
@@ -1,13 +1,17 @@
import { GlProgressBar } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import LearnGitlab from '~/pages/projects/learn_gitlab/components/learn_gitlab.vue';
+import eventHub from '~/invite_members/event_hub';
import { testActions, testSections } from './mock_data';
describe('Learn GitLab', () => {
let wrapper;
+ let inviteMembersOpen = false;
const createWrapper = () => {
- wrapper = mount(LearnGitlab, { propsData: { actions: testActions, sections: testSections } });
+ wrapper = mount(LearnGitlab, {
+ propsData: { actions: testActions, sections: testSections, inviteMembersOpen },
+ });
};
beforeEach(() => {
@@ -17,6 +21,7 @@ describe('Learn GitLab', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
+ inviteMembersOpen = false;
});
it('renders correctly', () => {
@@ -35,4 +40,30 @@ describe('Learn GitLab', () => {
expect(progressBar.attributes('value')).toBe('2');
expect(progressBar.attributes('max')).toBe('9');
});
+
+ describe('Invite Members Modal', () => {
+ let spy;
+
+ beforeEach(() => {
+ spy = jest.spyOn(eventHub, '$emit');
+ });
+
+ it('emits openModal', () => {
+ inviteMembersOpen = true;
+
+ createWrapper();
+
+ expect(spy).toHaveBeenCalledWith('openModal', {
+ mode: 'celebrate',
+ inviteeType: 'members',
+ source: 'learn-gitlab',
+ });
+ });
+
+ it('does not emit openModal', () => {
+ createWrapper();
+
+ expect(spy).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
index 082a8977710..9d510b3d231 100644
--- a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
+++ b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
@@ -8,9 +8,11 @@ import waitForPromises from 'helpers/wait_for_promises';
import ContentEditor from '~/content_editor/components/content_editor.vue';
import WikiForm from '~/pages/shared/wikis/components/wiki_form.vue';
import {
- WIKI_CONTENT_EDITOR_TRACKING_LABEL,
CONTENT_EDITOR_LOADED_ACTION,
SAVED_USING_CONTENT_EDITOR_ACTION,
+ WIKI_CONTENT_EDITOR_TRACKING_LABEL,
+ WIKI_FORMAT_LABEL,
+ WIKI_FORMAT_UPDATED_ACTION,
} from '~/pages/shared/wikis/constants';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
@@ -65,7 +67,6 @@ describe('WikiForm', () => {
const pageInfoPersisted = {
...pageInfoNew,
persisted: true,
-
title: 'My page',
content: ' My page content ',
format: 'markdown',
@@ -177,7 +178,7 @@ describe('WikiForm', () => {
await wrapper.vm.$nextTick();
expect(wrapper.text()).toContain(titleHelpText);
- expect(findTitleHelpLink().attributes().href).toEqual(titleHelpLink);
+ expect(findTitleHelpLink().attributes().href).toBe(titleHelpLink);
},
);
@@ -186,7 +187,7 @@ describe('WikiForm', () => {
await wrapper.vm.$nextTick();
- expect(findMarkdownHelpLink().attributes().href).toEqual(
+ expect(findMarkdownHelpLink().attributes().href).toBe(
'/help/user/markdown#wiki-specific-markdown',
);
});
@@ -220,8 +221,8 @@ describe('WikiForm', () => {
expect(e.preventDefault).not.toHaveBeenCalled();
});
- it('does not trigger tracking event', async () => {
- expect(trackingSpy).not.toHaveBeenCalled();
+ it('triggers wiki format tracking event', async () => {
+ expect(trackingSpy).toHaveBeenCalledTimes(1);
});
it('does not trim page content', () => {
@@ -273,7 +274,7 @@ describe('WikiForm', () => {
({ persisted, redirectLink }) => {
createWrapper(persisted);
- expect(findCancelButton().attributes().href).toEqual(redirectLink);
+ expect(findCancelButton().attributes().href).toBe(redirectLink);
},
);
});
@@ -438,7 +439,7 @@ describe('WikiForm', () => {
});
});
- it('triggers tracking event on form submit', async () => {
+ it('triggers tracking events on form submit', async () => {
triggerFormSubmit();
await wrapper.vm.$nextTick();
@@ -446,6 +447,15 @@ describe('WikiForm', () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, SAVED_USING_CONTENT_EDITOR_ACTION, {
label: WIKI_CONTENT_EDITOR_TRACKING_LABEL,
});
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, WIKI_FORMAT_UPDATED_ACTION, {
+ label: WIKI_FORMAT_LABEL,
+ value: findFormat().element.value,
+ extra: {
+ old_format: pageInfoPersisted.format,
+ project_path: pageInfoPersisted.path,
+ },
+ });
});
it('updates content from content editor on form submit', async () => {
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 8040c9d701c..23219042008 100644
--- a/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js
+++ b/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js
@@ -5,6 +5,9 @@ import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
import { mockCommitMessage, mockDefaultBranch } from '../../mock_data';
+const scrollIntoViewMock = jest.fn();
+HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
+
describe('Pipeline Editor | Commit Form', () => {
let wrapper;
@@ -113,4 +116,20 @@ describe('Pipeline Editor | Commit Form', () => {
expect(findSubmitBtn().attributes('disabled')).toBe('disabled');
});
});
+
+ describe('when scrollToCommitForm becomes true', () => {
+ beforeEach(async () => {
+ createComponent();
+ wrapper.setProps({ scrollToCommitForm: true });
+ await wrapper.vm.$nextTick();
+ });
+
+ it('scrolls into view', () => {
+ expect(scrollIntoViewMock).toHaveBeenCalledWith({ behavior: 'smooth' });
+ });
+
+ it('emits "scrolled-to-commit-form"', () => {
+ expect(wrapper.emitted()['scrolled-to-commit-form']).toBeTruthy();
+ });
+ });
});
diff --git a/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js b/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js
index 2f934898ef1..efc345d8877 100644
--- a/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js
+++ b/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js
@@ -52,6 +52,7 @@ describe('Pipeline Editor | Commit section', () => {
const defaultProps = {
ciFileContent: mockCiYml,
commitSha: mockCommitSha,
+ isNewCiConfigFile: false,
};
const createComponent = ({ props = {}, options = {}, provide = {} } = {}) => {
@@ -72,7 +73,6 @@ describe('Pipeline Editor | Commit section', () => {
data() {
return {
currentBranch: mockDefaultBranch,
- isNewCiConfigFile: Boolean(options?.isNewCiConfigfile),
};
},
mocks: {
@@ -115,7 +115,7 @@ describe('Pipeline Editor | Commit section', () => {
describe('when the user commits a new file', () => {
beforeEach(async () => {
- createComponent({ options: { isNewCiConfigfile: true } });
+ createComponent({ props: { isNewCiConfigFile: true } });
await submitCommit();
});
@@ -277,4 +277,16 @@ describe('Pipeline Editor | Commit section', () => {
expect(wrapper.emitted('resetContent')).toHaveLength(1);
});
});
+
+ it('sets listeners on commit form', () => {
+ const handler = jest.fn();
+ createComponent({ options: { listeners: { event: handler } } });
+ findCommitForm().vm.$emit('event');
+ expect(handler).toHaveBeenCalled();
+ });
+
+ it('passes down scroll-to-commit-form prop to commit form', () => {
+ createComponent({ props: { 'scroll-to-commit-form': true } });
+ expect(findCommitForm().props('scrollToCommitForm')).toBe(true);
+ });
});
diff --git a/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js b/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js
index 1b68cd3dc43..4df7768b035 100644
--- a/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js
+++ b/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js
@@ -1,6 +1,7 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { stubExperiments } from 'helpers/experimentation_helper';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import FirstPipelineCard from '~/pipeline_editor/components/drawer/cards/first_pipeline_card.vue';
import GettingStartedCard from '~/pipeline_editor/components/drawer/cards/getting_started_card.vue';
@@ -33,19 +34,41 @@ describe('Pipeline editor drawer', () => {
const clickToggleBtn = async () => findToggleBtn().vm.$emit('click');
+ const originalObjects = [];
+
+ beforeEach(() => {
+ originalObjects.push(window.gon, window.gl);
+ stubExperiments({ pipeline_editor_walkthrough: 'control' });
+ });
+
afterEach(() => {
wrapper.destroy();
localStorage.clear();
+ [window.gon, window.gl] = originalObjects;
});
- it('it sets the drawer to be opened by default', async () => {
- createComponent();
-
- expect(findDrawerContent().exists()).toBe(false);
-
- await nextTick();
+ describe('default expanded state', () => {
+ describe('when experiment control', () => {
+ it('sets the drawer to be opened by default', async () => {
+ createComponent();
+ expect(findDrawerContent().exists()).toBe(false);
+ await nextTick();
+ expect(findDrawerContent().exists()).toBe(true);
+ });
+ });
- expect(findDrawerContent().exists()).toBe(true);
+ describe('when experiment candidate', () => {
+ beforeEach(() => {
+ stubExperiments({ pipeline_editor_walkthrough: 'candidate' });
+ });
+
+ it('sets the drawer to be closed by default', async () => {
+ createComponent();
+ expect(findDrawerContent().exists()).toBe(false);
+ await nextTick();
+ expect(findDrawerContent().exists()).toBe(false);
+ });
+ });
});
describe('when the drawer is collapsed', () => {
diff --git a/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js b/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js
index b5881790b0b..6532c4e289d 100644
--- a/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js
+++ b/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js
@@ -36,8 +36,9 @@ describe('Pipeline editor branch switcher', () => {
let mockLastCommitBranchQuery;
const createComponent = (
- { currentBranch, isQueryLoading, mountFn, options } = {
+ { currentBranch, isQueryLoading, mountFn, options, props } = {
currentBranch: mockDefaultBranch,
+ hasUnsavedChanges: false,
isQueryLoading: false,
mountFn: shallowMount,
options: {},
@@ -45,6 +46,7 @@ describe('Pipeline editor branch switcher', () => {
) => {
wrapper = mountFn(BranchSwitcher, {
propsData: {
+ ...props,
paginationLimit: mockBranchPaginationLimit,
},
provide: {
@@ -70,7 +72,7 @@ describe('Pipeline editor branch switcher', () => {
});
};
- const createComponentWithApollo = (mountFn = shallowMount) => {
+ const createComponentWithApollo = ({ mountFn = shallowMount, props = {} } = {}) => {
const handlers = [[getAvailableBranchesQuery, mockAvailableBranchQuery]];
const resolvers = {
Query: {
@@ -86,6 +88,7 @@ describe('Pipeline editor branch switcher', () => {
createComponent({
mountFn,
+ props,
options: {
localVue,
apolloProvider: mockApollo,
@@ -138,8 +141,8 @@ describe('Pipeline editor branch switcher', () => {
createComponentWithApollo();
});
- it('does not render dropdown', () => {
- expect(findDropdown().exists()).toBe(false);
+ it('disables the dropdown', () => {
+ expect(findDropdown().props('disabled')).toBe(true);
});
});
@@ -149,7 +152,7 @@ describe('Pipeline editor branch switcher', () => {
availableBranches: mockProjectBranches,
currentBranch: mockDefaultBranch,
});
- createComponentWithApollo(mount);
+ createComponentWithApollo({ mountFn: mount });
await waitForPromises();
});
@@ -186,7 +189,7 @@ describe('Pipeline editor branch switcher', () => {
});
it('does not render dropdown', () => {
- expect(findDropdown().exists()).toBe(false);
+ expect(findDropdown().props('disabled')).toBe(true);
});
it('shows an error message', () => {
@@ -201,7 +204,7 @@ describe('Pipeline editor branch switcher', () => {
availableBranches: mockProjectBranches,
currentBranch: mockDefaultBranch,
});
- createComponentWithApollo(mount);
+ createComponentWithApollo({ mountFn: mount });
await waitForPromises();
});
@@ -247,6 +250,23 @@ describe('Pipeline editor branch switcher', () => {
expect(wrapper.emitted('refetchContent')).toBeUndefined();
});
+
+ describe('with unsaved changes', () => {
+ beforeEach(async () => {
+ createComponentWithApollo({ mountFn: mount, props: { hasUnsavedChanges: true } });
+ await waitForPromises();
+ });
+
+ it('emits `select-branch` event and does not switch branch', async () => {
+ expect(wrapper.emitted('select-branch')).toBeUndefined();
+
+ const branch = findDropdownItems().at(1);
+ await branch.vm.$emit('click');
+
+ expect(wrapper.emitted('select-branch')).toEqual([[branch.text()]]);
+ expect(wrapper.emitted('refetchContent')).toBeUndefined();
+ });
+ });
});
describe('when searching', () => {
@@ -255,7 +275,7 @@ describe('Pipeline editor branch switcher', () => {
availableBranches: mockProjectBranches,
currentBranch: mockDefaultBranch,
});
- createComponentWithApollo(mount);
+ createComponentWithApollo({ mountFn: mount });
await waitForPromises();
});
@@ -429,7 +449,7 @@ describe('Pipeline editor branch switcher', () => {
availableBranches: mockProjectBranches,
currentBranch: mockDefaultBranch,
});
- createComponentWithApollo(mount);
+ createComponentWithApollo({ mountFn: mount });
await waitForPromises();
await createNewBranch();
});
diff --git a/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js b/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js
index 44656b2b67d..29ab52bde8f 100644
--- a/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js
+++ b/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js
@@ -16,7 +16,7 @@ describe('Pipeline Status', () => {
let mockApollo;
let mockPipelineQuery;
- const createComponentWithApollo = (glFeatures = {}) => {
+ const createComponentWithApollo = () => {
const handlers = [[getPipelineQuery, mockPipelineQuery]];
mockApollo = createMockApollo(handlers);
@@ -27,7 +27,6 @@ describe('Pipeline Status', () => {
commitSha: mockCommitSha,
},
provide: {
- glFeatures,
projectFullPath: mockProjectFullPath,
},
stubs: { GlLink, GlSprintf },
@@ -40,6 +39,8 @@ describe('Pipeline Status', () => {
const findPipelineId = () => wrapper.find('[data-testid="pipeline-id"]');
const findPipelineCommit = () => wrapper.find('[data-testid="pipeline-commit"]');
const findPipelineErrorMsg = () => wrapper.find('[data-testid="pipeline-error-msg"]');
+ const findPipelineNotTriggeredErrorMsg = () =>
+ wrapper.find('[data-testid="pipeline-not-triggered-error-msg"]');
const findPipelineLoadingMsg = () => wrapper.find('[data-testid="pipeline-loading-msg"]');
const findPipelineViewBtn = () => wrapper.find('[data-testid="pipeline-view-btn"]');
const findStatusIcon = () => wrapper.find('[data-testid="pipeline-status-icon"]');
@@ -95,17 +96,18 @@ describe('Pipeline Status', () => {
it('renders pipeline data', () => {
const {
id,
+ commit: { title },
detailedStatus: { detailsPath },
} = mockProjectPipeline().pipeline;
expect(findStatusIcon().exists()).toBe(true);
expect(findPipelineId().text()).toBe(`#${id.match(/\d+/g)[0]}`);
- expect(findPipelineCommit().text()).toBe(mockCommitSha);
+ expect(findPipelineCommit().text()).toBe(`${mockCommitSha}: ${title}`);
expect(findPipelineViewBtn().attributes('href')).toBe(detailsPath);
});
- it('does not render the pipeline mini graph', () => {
- expect(findPipelineEditorMiniGraph().exists()).toBe(false);
+ it('renders the pipeline mini graph', () => {
+ expect(findPipelineEditorMiniGraph().exists()).toBe(true);
});
});
@@ -117,7 +119,8 @@ describe('Pipeline Status', () => {
await waitForPromises();
});
- it('renders error', () => {
+ it('renders api error', () => {
+ expect(findPipelineNotTriggeredErrorMsg().exists()).toBe(false);
expect(findIcon().attributes('name')).toBe('warning-solid');
expect(findPipelineErrorMsg().text()).toBe(i18n.fetchError);
});
@@ -129,20 +132,22 @@ describe('Pipeline Status', () => {
expect(findPipelineViewBtn().exists()).toBe(false);
});
});
- });
- describe('when feature flag for pipeline mini graph is enabled', () => {
- beforeEach(() => {
- mockPipelineQuery.mockResolvedValue({
- data: { project: mockProjectPipeline() },
- });
+ describe('when pipeline is null', () => {
+ beforeEach(() => {
+ mockPipelineQuery.mockResolvedValue({
+ data: { project: { pipeline: null } },
+ });
- createComponentWithApollo({ pipelineEditorMiniGraph: true });
- waitForPromises();
- });
+ createComponentWithApollo();
+ waitForPromises();
+ });
- it('renders the pipeline mini graph', () => {
- expect(findPipelineEditorMiniGraph().exists()).toBe(true);
+ it('renders pipeline not triggered error', () => {
+ expect(findPipelineErrorMsg().exists()).toBe(false);
+ expect(findIcon().attributes('name')).toBe('information-o');
+ expect(findPipelineNotTriggeredErrorMsg().text()).toBe(i18n.pipelineNotTriggeredMsg);
+ });
});
});
});
diff --git a/spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js b/spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js
index 3d7c3c839da..6b9f576917f 100644
--- a/spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js
+++ b/spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js
@@ -1,22 +1,54 @@
-import { shallowMount } from '@vue/test-utils';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
import PipelineEditorMiniGraph from '~/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue';
-import { mockProjectPipeline } from '../../mock_data';
+import getLinkedPipelinesQuery from '~/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql';
+import { PIPELINE_FAILURE } from '~/pipeline_editor/constants';
+import { mockLinkedPipelines, mockProjectFullPath, mockProjectPipeline } from '../../mock_data';
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
describe('Pipeline Status', () => {
let wrapper;
+ let mockApollo;
+ let mockLinkedPipelinesQuery;
- const createComponent = ({ hasStages = true } = {}) => {
+ const createComponent = ({ hasStages = true, options } = {}) => {
wrapper = shallowMount(PipelineEditorMiniGraph, {
+ provide: {
+ dataMethod: 'graphql',
+ projectFullPath: mockProjectFullPath,
+ },
propsData: {
pipeline: mockProjectPipeline({ hasStages }).pipeline,
},
+ ...options,
+ });
+ };
+
+ const createComponentWithApollo = (hasStages = true) => {
+ const handlers = [[getLinkedPipelinesQuery, mockLinkedPipelinesQuery]];
+ mockApollo = createMockApollo(handlers);
+
+ createComponent({
+ hasStages,
+ options: {
+ localVue,
+ apolloProvider: mockApollo,
+ },
});
};
const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph);
+ beforeEach(() => {
+ mockLinkedPipelinesQuery = jest.fn();
+ });
+
afterEach(() => {
+ mockLinkedPipelinesQuery.mockReset();
wrapper.destroy();
});
@@ -39,4 +71,38 @@ describe('Pipeline Status', () => {
expect(findPipelineMiniGraph().exists()).toBe(false);
});
});
+
+ describe('when querying upstream and downstream pipelines', () => {
+ describe('when query succeeds', () => {
+ beforeEach(() => {
+ mockLinkedPipelinesQuery.mockResolvedValue(mockLinkedPipelines());
+ createComponentWithApollo();
+ });
+
+ it('should call the query with the correct variables', () => {
+ expect(mockLinkedPipelinesQuery).toHaveBeenCalledTimes(1);
+ expect(mockLinkedPipelinesQuery).toHaveBeenCalledWith({
+ fullPath: mockProjectFullPath,
+ iid: mockProjectPipeline().pipeline.iid,
+ });
+ });
+ });
+
+ describe('when query fails', () => {
+ beforeEach(() => {
+ mockLinkedPipelinesQuery.mockRejectedValue(new Error());
+ createComponentWithApollo();
+ });
+
+ it('should emit an error event when query fails', async () => {
+ expect(wrapper.emitted('showError')).toHaveLength(1);
+ expect(wrapper.emitted('showError')[0]).toEqual([
+ {
+ type: PIPELINE_FAILURE,
+ reasons: [wrapper.vm.$options.i18n.linkedPipelinesFetchError],
+ },
+ ]);
+ });
+ });
+ });
});
diff --git a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
index 5cf8d47bc23..f6154f50bc0 100644
--- a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
+++ b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
@@ -1,19 +1,27 @@
-import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
+import { GlAlert, GlLoadingIcon, GlTabs } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import setWindowLocation from 'helpers/set_window_location_helper';
import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue';
+import WalkthroughPopover from '~/pipeline_editor/components/walkthrough_popover.vue';
import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue';
import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
import EditorTab from '~/pipeline_editor/components/ui/editor_tab.vue';
+import { stubExperiments } from 'helpers/experimentation_helper';
import {
+ CREATE_TAB,
EDITOR_APP_STATUS_EMPTY,
- EDITOR_APP_STATUS_ERROR,
EDITOR_APP_STATUS_LOADING,
EDITOR_APP_STATUS_INVALID,
EDITOR_APP_STATUS_VALID,
+ MERGED_TAB,
+ TAB_QUERY_PARAM,
+ TABS_INDEX,
} from '~/pipeline_editor/constants';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
-import { mockLintResponse, mockCiYml } from '../mock_data';
+import { mockLintResponse, mockLintResponseWithoutMerged, mockCiYml } from '../mock_data';
+
+Vue.config.ignoredElements = ['gl-emoji'];
describe('Pipeline editor tabs component', () => {
let wrapper;
@@ -22,6 +30,7 @@ describe('Pipeline editor tabs component', () => {
};
const createComponent = ({
+ listeners = {},
props = {},
provide = {},
appStatus = EDITOR_APP_STATUS_VALID,
@@ -31,6 +40,7 @@ describe('Pipeline editor tabs component', () => {
propsData: {
ciConfigData: mockLintResponse,
ciFileContent: mockCiYml,
+ isNewCiConfigFile: true,
...props,
},
data() {
@@ -43,6 +53,7 @@ describe('Pipeline editor tabs component', () => {
TextEditor: MockTextEditor,
EditorTab,
},
+ listeners,
});
};
@@ -53,10 +64,12 @@ describe('Pipeline editor tabs component', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
const findCiLint = () => wrapper.findComponent(CiLint);
+ const findGlTabs = () => wrapper.findComponent(GlTabs);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findPipelineGraph = () => wrapper.findComponent(PipelineGraph);
const findTextEditor = () => wrapper.findComponent(MockTextEditor);
const findMergedPreview = () => wrapper.findComponent(CiConfigMergedPreview);
+ const findWalkthroughPopover = () => wrapper.findComponent(WalkthroughPopover);
afterEach(() => {
wrapper.destroy();
@@ -137,7 +150,7 @@ describe('Pipeline editor tabs component', () => {
describe('when there is a fetch error', () => {
beforeEach(() => {
- createComponent({ appStatus: EDITOR_APP_STATUS_ERROR });
+ createComponent({ props: { ciConfigData: mockLintResponseWithoutMerged } });
});
it('show an error message', () => {
@@ -181,4 +194,113 @@ describe('Pipeline editor tabs component', () => {
},
);
});
+
+ describe('default tab based on url query param', () => {
+ const gitlabUrl = 'https://gitlab.test/ci/editor/';
+ const matchObject = {
+ hostname: 'gitlab.test',
+ pathname: '/ci/editor/',
+ search: '',
+ };
+
+ it(`is ${CREATE_TAB} if the query param ${TAB_QUERY_PARAM} is not present`, () => {
+ setWindowLocation(gitlabUrl);
+ createComponent();
+
+ expect(window.location).toMatchObject(matchObject);
+ });
+
+ it(`is ${CREATE_TAB} tab if the query param ${TAB_QUERY_PARAM} is invalid`, () => {
+ const queryValue = 'FOO';
+ setWindowLocation(`${gitlabUrl}?${TAB_QUERY_PARAM}=${queryValue}`);
+ createComponent();
+
+ // If the query param remains unchanged, then we have ignored it.
+ expect(window.location).toMatchObject({
+ ...matchObject,
+ search: `?${TAB_QUERY_PARAM}=${queryValue}`,
+ });
+ });
+
+ it('is the tab specified in query param and transform it into an index value', async () => {
+ setWindowLocation(`${gitlabUrl}?${TAB_QUERY_PARAM}=${MERGED_TAB}`);
+ createComponent();
+
+ // If the query param has changed to an index, it means we have synced the
+ // query with.
+ expect(window.location).toMatchObject({
+ ...matchObject,
+ search: `?${TAB_QUERY_PARAM}=${TABS_INDEX[MERGED_TAB]}`,
+ });
+ });
+ });
+
+ describe('glTabs', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('passes the `sync-active-tab-with-query-params` prop', () => {
+ expect(findGlTabs().props('syncActiveTabWithQueryParams')).toBe(true);
+ });
+ });
+
+ describe('pipeline_editor_walkthrough experiment', () => {
+ describe('when in control path', () => {
+ beforeEach(() => {
+ stubExperiments({ pipeline_editor_walkthrough: 'control' });
+ });
+
+ it('does not show walkthrough popover', async () => {
+ createComponent({ mountFn: mount });
+ await nextTick();
+ expect(findWalkthroughPopover().exists()).toBe(false);
+ });
+ });
+
+ describe('when in candidate path', () => {
+ beforeEach(() => {
+ stubExperiments({ pipeline_editor_walkthrough: 'candidate' });
+ });
+
+ describe('when isNewCiConfigFile prop is true (default)', () => {
+ beforeEach(async () => {
+ createComponent({
+ mountFn: mount,
+ });
+ await nextTick();
+ });
+
+ it('shows walkthrough popover', async () => {
+ expect(findWalkthroughPopover().exists()).toBe(true);
+ });
+ });
+
+ describe('when isNewCiConfigFile prop is false', () => {
+ it('does not show walkthrough popover', async () => {
+ createComponent({ props: { isNewCiConfigFile: false }, mountFn: mount });
+ await nextTick();
+ expect(findWalkthroughPopover().exists()).toBe(false);
+ });
+ });
+ });
+ });
+
+ it('sets listeners on walkthrough popover', async () => {
+ stubExperiments({ pipeline_editor_walkthrough: 'candidate' });
+
+ const handler = jest.fn();
+
+ createComponent({
+ mountFn: mount,
+ listeners: {
+ event: handler,
+ },
+ });
+ await nextTick();
+
+ findWalkthroughPopover().vm.$emit('event');
+
+ expect(handler).toHaveBeenCalled();
+ });
});
diff --git a/spec/frontend/pipeline_editor/components/ui/pipeline_editor_messages_spec.js b/spec/frontend/pipeline_editor/components/ui/pipeline_editor_messages_spec.js
index 9f910ed4f9c..a55176ccd79 100644
--- a/spec/frontend/pipeline_editor/components/ui/pipeline_editor_messages_spec.js
+++ b/spec/frontend/pipeline_editor/components/ui/pipeline_editor_messages_spec.js
@@ -11,6 +11,7 @@ import {
DEFAULT_FAILURE,
DEFAULT_SUCCESS,
LOAD_FAILURE_UNKNOWN,
+ PIPELINE_FAILURE,
} from '~/pipeline_editor/constants';
beforeEach(() => {
@@ -65,6 +66,7 @@ describe('Pipeline Editor messages', () => {
failureType | message | expectedFailureType
${COMMIT_FAILURE} | ${'failed commit'} | ${COMMIT_FAILURE}
${LOAD_FAILURE_UNKNOWN} | ${'loading failure'} | ${LOAD_FAILURE_UNKNOWN}
+ ${PIPELINE_FAILURE} | ${'pipeline failure'} | ${PIPELINE_FAILURE}
${'random'} | ${'error without a specified type'} | ${DEFAULT_FAILURE}
`('shows a message for $message', ({ failureType, expectedFailureType }) => {
createComponent({ failureType, showFailure: true });
diff --git a/spec/frontend/pipeline_editor/components/walkthrough_popover_spec.js b/spec/frontend/pipeline_editor/components/walkthrough_popover_spec.js
new file mode 100644
index 00000000000..a9ce89ff521
--- /dev/null
+++ b/spec/frontend/pipeline_editor/components/walkthrough_popover_spec.js
@@ -0,0 +1,29 @@
+import { mount, shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import WalkthroughPopover from '~/pipeline_editor/components/walkthrough_popover.vue';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+
+Vue.config.ignoredElements = ['gl-emoji'];
+
+describe('WalkthroughPopover component', () => {
+ let wrapper;
+
+ const createComponent = (mountFn = shallowMount) => {
+ return extendedWrapper(mountFn(WalkthroughPopover));
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('CTA button clicked', () => {
+ beforeEach(async () => {
+ wrapper = createComponent(mount);
+ await wrapper.findByTestId('ctaBtn').trigger('click');
+ });
+
+ it('emits "walkthrough-popover-cta-clicked" event', async () => {
+ expect(wrapper.emitted()['walkthrough-popover-cta-clicked']).toBeTruthy();
+ });
+ });
+});
diff --git a/spec/frontend/pipeline_editor/mock_data.js b/spec/frontend/pipeline_editor/mock_data.js
index 0b0ff14486e..1bfc5c3b93d 100644
--- a/spec/frontend/pipeline_editor/mock_data.js
+++ b/spec/frontend/pipeline_editor/mock_data.js
@@ -1,4 +1,4 @@
-import { CI_CONFIG_STATUS_VALID } from '~/pipeline_editor/constants';
+import { CI_CONFIG_STATUS_INVALID, CI_CONFIG_STATUS_VALID } from '~/pipeline_editor/constants';
import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils';
export const mockProjectNamespace = 'user1';
@@ -35,6 +35,17 @@ job_build:
- echo "build"
needs: ["job_test_2"]
`;
+
+export const mockCiTemplateQueryResponse = {
+ data: {
+ project: {
+ ciTemplate: {
+ content: mockCiYml,
+ },
+ },
+ },
+};
+
export const mockBlobContentQueryResponse = {
data: {
project: { repository: { blobs: { nodes: [{ rawBlob: mockCiYml }] } } },
@@ -274,11 +285,14 @@ export const mockProjectPipeline = ({ hasStages = true } = {}) => {
return {
pipeline: {
- commitPath: '/-/commit/aabbccdd',
id: 'gid://gitlab/Ci::Pipeline/118',
iid: '28',
shortSha: mockCommitSha,
status: 'SUCCESS',
+ commit: {
+ title: 'Update .gitlabe-ci.yml',
+ webPath: '/-/commit/aabbccdd',
+ },
detailedStatus: {
detailsPath: '/root/sample-ci-project/-/pipelines/118',
group: 'success',
@@ -290,6 +304,62 @@ export const mockProjectPipeline = ({ hasStages = true } = {}) => {
};
};
+export const mockLinkedPipelines = ({ hasDownstream = true, hasUpstream = true } = {}) => {
+ let upstream = null;
+ let downstream = {
+ nodes: [],
+ __typename: 'PipelineConnection',
+ };
+
+ if (hasDownstream) {
+ downstream = {
+ nodes: [
+ {
+ id: 'gid://gitlab/Ci::Pipeline/612',
+ path: '/root/job-log-sections/-/pipelines/612',
+ project: { name: 'job-log-sections', __typename: 'Project' },
+ detailedStatus: {
+ group: 'success',
+ icon: 'status_success',
+ label: 'passed',
+ __typename: 'DetailedStatus',
+ },
+ __typename: 'Pipeline',
+ },
+ ],
+ __typename: 'PipelineConnection',
+ };
+ }
+
+ if (hasUpstream) {
+ upstream = {
+ id: 'gid://gitlab/Ci::Pipeline/610',
+ path: '/root/trigger-downstream/-/pipelines/610',
+ project: { name: 'trigger-downstream', __typename: 'Project' },
+ detailedStatus: {
+ group: 'success',
+ icon: 'status_success',
+ label: 'passed',
+ __typename: 'DetailedStatus',
+ },
+ __typename: 'Pipeline',
+ };
+ }
+
+ return {
+ data: {
+ project: {
+ pipeline: {
+ path: '/root/ci-project/-/pipelines/790',
+ downstream,
+ upstream,
+ },
+ __typename: 'Project',
+ },
+ },
+ };
+};
+
export const mockLintResponse = {
valid: true,
mergedYaml: mockCiYml,
@@ -326,6 +396,14 @@ export const mockLintResponse = {
],
};
+export const mockLintResponseWithoutMerged = {
+ valid: false,
+ status: CI_CONFIG_STATUS_INVALID,
+ errors: ['error'],
+ warnings: [],
+ jobs: [],
+};
+
export const mockJobs = [
{
name: 'job_1',
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
index b6713319e69..f6afef595c6 100644
--- a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
+++ b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
@@ -1,11 +1,9 @@
-import { GlAlert, GlButton, GlLoadingIcon, GlTabs } from '@gitlab/ui';
+import { GlAlert, GlButton, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
-import TextEditor from '~/pipeline_editor/components/editor/text_editor.vue';
import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
import PipelineEditorEmptyState from '~/pipeline_editor/components/ui/pipeline_editor_empty_state.vue';
@@ -13,17 +11,21 @@ import PipelineEditorMessages from '~/pipeline_editor/components/ui/pipeline_edi
import { COMMIT_SUCCESS, COMMIT_FAILURE } from '~/pipeline_editor/constants';
import getBlobContent from '~/pipeline_editor/graphql/queries/blob_content.graphql';
import getCiConfigData from '~/pipeline_editor/graphql/queries/ci_config.graphql';
-import getPipelineQuery from '~/pipeline_editor/graphql/queries/client/pipeline.graphql';
import getTemplate from '~/pipeline_editor/graphql/queries/get_starter_template.query.graphql';
import getLatestCommitShaQuery from '~/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql';
+
+import getPipelineQuery from '~/pipeline_editor/graphql/queries/client/pipeline.graphql';
+
import PipelineEditorApp from '~/pipeline_editor/pipeline_editor_app.vue';
import PipelineEditorHome from '~/pipeline_editor/pipeline_editor_home.vue';
+
import {
mockCiConfigPath,
mockCiConfigQueryResponse,
mockBlobContentQueryResponse,
mockBlobContentQueryResponseNoCiFile,
mockCiYml,
+ mockCiTemplateQueryResponse,
mockCommitSha,
mockCommitShaResults,
mockDefaultBranch,
@@ -35,10 +37,6 @@ import {
const localVue = createLocalVue();
localVue.use(VueApollo);
-const MockSourceEditor = {
- template: '<div/>',
-};
-
const mockProvide = {
ciConfigPath: mockCiConfigPath,
defaultBranch: mockDefaultBranch,
@@ -55,19 +53,15 @@ describe('Pipeline editor app component', () => {
let mockLatestCommitShaQuery;
let mockPipelineQuery;
- const createComponent = ({ blobLoading = false, options = {}, provide = {} } = {}) => {
+ const createComponent = ({
+ blobLoading = false,
+ options = {},
+ provide = {},
+ stubs = {},
+ } = {}) => {
wrapper = shallowMount(PipelineEditorApp, {
provide: { ...mockProvide, ...provide },
- stubs: {
- GlTabs,
- GlButton,
- CommitForm,
- PipelineEditorHome,
- PipelineEditorTabs,
- PipelineEditorMessages,
- SourceEditor: MockSourceEditor,
- PipelineEditorEmptyState,
- },
+ stubs,
data() {
return {
commitSha: '',
@@ -89,7 +83,7 @@ describe('Pipeline editor app component', () => {
});
};
- const createComponentWithApollo = async ({ props = {}, provide = {} } = {}) => {
+ const createComponentWithApollo = async ({ provide = {}, stubs = {} } = {}) => {
const handlers = [
[getBlobContent, mockBlobContentData],
[getCiConfigData, mockCiConfigData],
@@ -97,7 +91,6 @@ describe('Pipeline editor app component', () => {
[getLatestCommitShaQuery, mockLatestCommitShaQuery],
[getPipelineQuery, mockPipelineQuery],
];
-
mockApollo = createMockApollo(handlers);
const options = {
@@ -105,13 +98,15 @@ describe('Pipeline editor app component', () => {
data() {
return {
currentBranch: mockDefaultBranch,
+ lastCommitBranch: '',
+ appStatus: '',
};
},
mocks: {},
apolloProvider: mockApollo,
};
- createComponent({ props, provide, options });
+ createComponent({ provide, stubs, options });
return waitForPromises();
};
@@ -119,7 +114,6 @@ describe('Pipeline editor app component', () => {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findAlert = () => wrapper.findComponent(GlAlert);
const findEditorHome = () => wrapper.findComponent(PipelineEditorHome);
- const findTextEditor = () => wrapper.findComponent(TextEditor);
const findEmptyState = () => wrapper.findComponent(PipelineEditorEmptyState);
const findEmptyStateButton = () =>
wrapper.findComponent(PipelineEditorEmptyState).findComponent(GlButton);
@@ -141,7 +135,7 @@ describe('Pipeline editor app component', () => {
createComponent({ blobLoading: true });
expect(findLoadingIcon().exists()).toBe(true);
- expect(findTextEditor().exists()).toBe(false);
+ expect(findEditorHome().exists()).toBe(false);
});
});
@@ -185,7 +179,11 @@ describe('Pipeline editor app component', () => {
describe('when no CI config file exists', () => {
beforeEach(async () => {
mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile);
- await createComponentWithApollo();
+ await createComponentWithApollo({
+ stubs: {
+ PipelineEditorEmptyState,
+ },
+ });
jest
.spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling')
@@ -206,8 +204,12 @@ describe('Pipeline editor app component', () => {
it('shows a unkown error message', async () => {
const loadUnknownFailureText = 'The CI configuration was not loaded, please try again.';
- mockBlobContentData.mockRejectedValueOnce(new Error('My error!'));
- await createComponentWithApollo();
+ mockBlobContentData.mockRejectedValueOnce();
+ await createComponentWithApollo({
+ stubs: {
+ PipelineEditorMessages,
+ },
+ });
expect(findEmptyState().exists()).toBe(false);
@@ -222,15 +224,20 @@ describe('Pipeline editor app component', () => {
mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile);
mockLatestCommitShaQuery.mockResolvedValue(mockEmptyCommitShaResults);
- await createComponentWithApollo();
+ await createComponentWithApollo({
+ stubs: {
+ PipelineEditorHome,
+ PipelineEditorEmptyState,
+ },
+ });
expect(findEmptyState().exists()).toBe(true);
- expect(findTextEditor().exists()).toBe(false);
+ expect(findEditorHome().exists()).toBe(false);
await findEmptyStateButton().vm.$emit('click');
expect(findEmptyState().exists()).toBe(false);
- expect(findTextEditor().exists()).toBe(true);
+ expect(findEditorHome().exists()).toBe(true);
});
});
@@ -241,7 +248,7 @@ describe('Pipeline editor app component', () => {
describe('and the commit mutation succeeds', () => {
beforeEach(async () => {
window.scrollTo = jest.fn();
- await createComponentWithApollo();
+ await createComponentWithApollo({ stubs: { PipelineEditorMessages } });
findEditorHome().vm.$emit('commit', { type: COMMIT_SUCCESS });
});
@@ -295,7 +302,7 @@ describe('Pipeline editor app component', () => {
beforeEach(async () => {
window.scrollTo = jest.fn();
- await createComponentWithApollo();
+ await createComponentWithApollo({ stubs: { PipelineEditorMessages } });
findEditorHome().vm.$emit('showError', {
type: COMMIT_FAILURE,
@@ -319,7 +326,7 @@ describe('Pipeline editor app component', () => {
beforeEach(async () => {
window.scrollTo = jest.fn();
- await createComponentWithApollo();
+ await createComponentWithApollo({ stubs: { PipelineEditorMessages } });
findEditorHome().vm.$emit('showError', {
type: COMMIT_FAILURE,
@@ -342,6 +349,8 @@ describe('Pipeline editor app component', () => {
describe('when refetching content', () => {
beforeEach(() => {
+ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
+ mockCiConfigData.mockResolvedValue(mockCiConfigQueryResponse);
mockLatestCommitShaQuery.mockResolvedValue(mockCommitShaResults);
});
@@ -377,7 +386,10 @@ describe('Pipeline editor app component', () => {
const originalLocation = window.location.href;
beforeEach(() => {
+ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
+ mockCiConfigData.mockResolvedValue(mockCiConfigQueryResponse);
mockLatestCommitShaQuery.mockResolvedValue(mockCommitShaResults);
+ mockGetTemplate.mockResolvedValue(mockCiTemplateQueryResponse);
setWindowLocation('?template=Android');
});
@@ -386,7 +398,9 @@ describe('Pipeline editor app component', () => {
});
it('renders the given template', async () => {
- await createComponentWithApollo();
+ await createComponentWithApollo({
+ stubs: { PipelineEditorHome, PipelineEditorTabs },
+ });
expect(mockGetTemplate).toHaveBeenCalledWith({
projectPath: mockProjectFullPath,
@@ -394,7 +408,40 @@ describe('Pipeline editor app component', () => {
});
expect(findEmptyState().exists()).toBe(false);
- expect(findTextEditor().exists()).toBe(true);
+ expect(findEditorHome().exists()).toBe(true);
+ });
+ });
+
+ describe('when add_new_config_file query param is present', () => {
+ const originalLocation = window.location.href;
+
+ beforeEach(() => {
+ setWindowLocation('?add_new_config_file=true');
+
+ mockCiConfigData.mockResolvedValue(mockCiConfigQueryResponse);
+ });
+
+ afterEach(() => {
+ setWindowLocation(originalLocation);
+ });
+
+ describe('when CI config file does not exist', () => {
+ beforeEach(async () => {
+ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile);
+ mockLatestCommitShaQuery.mockResolvedValue(mockEmptyCommitShaResults);
+ mockGetTemplate.mockResolvedValue(mockCiTemplateQueryResponse);
+
+ await createComponentWithApollo();
+
+ jest
+ .spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling')
+ .mockImplementation(jest.fn());
+ });
+
+ it('skips empty state and shows editor home component', () => {
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findEditorHome().exists()).toBe(true);
+ });
});
});
});
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
index 335049892ec..6f969546171 100644
--- a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
+++ b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
@@ -1,21 +1,25 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
-
+import { GlModal } from '@gitlab/ui';
import CommitSection from '~/pipeline_editor/components/commit/commit_section.vue';
import PipelineEditorDrawer from '~/pipeline_editor/components/drawer/pipeline_editor_drawer.vue';
import PipelineEditorFileNav from '~/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue';
+import BranchSwitcher from '~/pipeline_editor/components/file_nav/branch_switcher.vue';
import PipelineEditorHeader from '~/pipeline_editor/components/header/pipeline_editor_header.vue';
import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
-import { MERGED_TAB, VISUALIZE_TAB } from '~/pipeline_editor/constants';
+import { MERGED_TAB, VISUALIZE_TAB, CREATE_TAB, LINT_TAB } from '~/pipeline_editor/constants';
import PipelineEditorHome from '~/pipeline_editor/pipeline_editor_home.vue';
import { mockLintResponse, mockCiYml } from './mock_data';
+jest.mock('~/lib/utils/common_utils');
+
describe('Pipeline editor home wrapper', () => {
let wrapper;
- const createComponent = ({ props = {}, glFeatures = {} } = {}) => {
+ const createComponent = ({ props = {}, glFeatures = {}, data = {}, stubs = {} } = {}) => {
wrapper = shallowMount(PipelineEditorHome, {
+ data: () => data,
propsData: {
ciConfigData: mockLintResponse,
ciFileContent: mockCiYml,
@@ -24,22 +28,26 @@ describe('Pipeline editor home wrapper', () => {
...props,
},
provide: {
+ projectFullPath: '',
+ totalBranches: 19,
glFeatures: {
...glFeatures,
},
},
+ stubs,
});
};
+ const findBranchSwitcher = () => wrapper.findComponent(BranchSwitcher);
const findCommitSection = () => wrapper.findComponent(CommitSection);
const findFileNav = () => wrapper.findComponent(PipelineEditorFileNav);
+ const findModal = () => wrapper.findComponent(GlModal);
const findPipelineEditorDrawer = () => wrapper.findComponent(PipelineEditorDrawer);
const findPipelineEditorHeader = () => wrapper.findComponent(PipelineEditorHeader);
const findPipelineEditorTabs = () => wrapper.findComponent(PipelineEditorTabs);
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
describe('renders', () => {
@@ -68,29 +76,103 @@ describe('Pipeline editor home wrapper', () => {
});
});
+ describe('modal when switching branch', () => {
+ describe('when `showSwitchBranchModal` value is false', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('is not visible', () => {
+ expect(findModal().exists()).toBe(false);
+ });
+ });
+ describe('when `showSwitchBranchModal` value is true', () => {
+ beforeEach(() => {
+ createComponent({
+ data: { showSwitchBranchModal: true },
+ stubs: { PipelineEditorFileNav },
+ });
+ });
+
+ it('is visible', () => {
+ expect(findModal().exists()).toBe(true);
+ });
+
+ it('pass down `shouldLoadNewBranch` to the branch switcher when primary is selected', async () => {
+ expect(findBranchSwitcher().props('shouldLoadNewBranch')).toBe(false);
+
+ await findModal().vm.$emit('primary');
+
+ expect(findBranchSwitcher().props('shouldLoadNewBranch')).toBe(true);
+ });
+
+ it('closes the modal when secondary action is selected', async () => {
+ expect(findModal().exists()).toBe(true);
+
+ await findModal().vm.$emit('secondary');
+
+ expect(findModal().exists()).toBe(false);
+ });
+ });
+ });
+
describe('commit form toggle', () => {
beforeEach(() => {
createComponent();
});
- it('hides the commit form when in the merged tab', async () => {
- expect(findCommitSection().exists()).toBe(true);
+ it.each`
+ tab | shouldShow
+ ${MERGED_TAB} | ${false}
+ ${VISUALIZE_TAB} | ${false}
+ ${LINT_TAB} | ${false}
+ ${CREATE_TAB} | ${true}
+ `(
+ 'when the active tab is $tab the commit form is shown: $shouldShow',
+ async ({ tab, shouldShow }) => {
+ 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', tab);
+
+ await nextTick();
- it('shows the form again when leaving the merged tab', async () => {
+ expect(findCommitSection().exists()).toBe(shouldShow);
+ },
+ );
+
+ it('shows the commit form again when coming back to the create 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);
+ findPipelineEditorTabs().vm.$emit('set-current-tab', CREATE_TAB);
await nextTick();
expect(findCommitSection().exists()).toBe(true);
});
});
+
+ describe('WalkthroughPopover events', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('when "walkthrough-popover-cta-clicked" is emitted from pipeline editor tabs', () => {
+ it('passes down `scrollToCommitForm=true` to commit section', async () => {
+ expect(findCommitSection().props('scrollToCommitForm')).toBe(false);
+ await findPipelineEditorTabs().vm.$emit('walkthrough-popover-cta-clicked');
+ expect(findCommitSection().props('scrollToCommitForm')).toBe(true);
+ });
+ });
+
+ describe('when "scrolled-to-commit-form" is emitted from commit section', () => {
+ it('passes down `scrollToCommitForm=false` to commit section', async () => {
+ await findPipelineEditorTabs().vm.$emit('walkthrough-popover-cta-clicked');
+ expect(findCommitSection().props('scrollToCommitForm')).toBe(true);
+ await findCommitSection().vm.$emit('scrolled-to-commit-form');
+ expect(findCommitSection().props('scrollToCommitForm')).toBe(false);
+ });
+ });
+ });
});
diff --git a/spec/frontend/pipelines/empty_state_spec.js b/spec/frontend/pipelines/empty_state_spec.js
index 1af3065477d..31b74a06efd 100644
--- a/spec/frontend/pipelines/empty_state_spec.js
+++ b/spec/frontend/pipelines/empty_state_spec.js
@@ -35,7 +35,7 @@ describe('Pipelines Empty State', () => {
});
it('should render the CI/CD templates', () => {
- expect(pipelinesCiTemplates()).toExist();
+ expect(pipelinesCiTemplates().exists()).toBe(true);
});
});
diff --git a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
index 2e8979f2b9d..db4de6deeb7 100644
--- a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
@@ -327,7 +327,7 @@ describe('Pipeline graph wrapper', () => {
expect(getLinksLayer().exists()).toBe(true);
expect(getLinksLayer().props('showLinks')).toBe(false);
expect(getViewSelector().props('type')).toBe(LAYER_VIEW);
- await getDependenciesToggle().trigger('click');
+ await getDependenciesToggle().vm.$emit('change', true);
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
expect(wrapper.findComponent(LinksLayer).props('showLinks')).toBe(true);
diff --git a/spec/frontend/pipelines/graph/graph_view_selector_spec.js b/spec/frontend/pipelines/graph/graph_view_selector_spec.js
index 5b2a29de443..f4faa25545b 100644
--- a/spec/frontend/pipelines/graph/graph_view_selector_spec.js
+++ b/spec/frontend/pipelines/graph/graph_view_selector_spec.js
@@ -111,7 +111,7 @@ describe('the graph view selector component', () => {
expect(wrapper.emitted().updateShowLinksState).toBeUndefined();
expect(findToggleLoader().exists()).toBe(false);
- await findDependenciesToggle().trigger('click');
+ await findDependenciesToggle().vm.$emit('change', true);
/*
Loading happens before the event is emitted or timers are run.
Then we run the timer because the event is emitted in setInterval
diff --git a/spec/frontend/pipelines/pipelines_artifacts_spec.js b/spec/frontend/pipelines/pipelines_artifacts_spec.js
index f33c66dedf3..2d876841e06 100644
--- a/spec/frontend/pipelines/pipelines_artifacts_spec.js
+++ b/spec/frontend/pipelines/pipelines_artifacts_spec.js
@@ -1,15 +1,9 @@
-import { GlAlert, GlDropdown, GlDropdownItem, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem, GlSprintf } 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 PipelineArtifacts, {
- i18n,
-} from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue';
+import PipelineArtifacts from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue';
describe('Pipelines Artifacts dropdown', () => {
let wrapper;
- let mockAxios;
const artifacts = [
{
@@ -21,23 +15,13 @@ describe('Pipelines Artifacts dropdown', () => {
path: '/download/path-two',
},
];
- const artifactsEndpointPlaceholder = ':pipeline_artifacts_id';
- const artifactsEndpoint = `endpoint/${artifactsEndpointPlaceholder}/artifacts.json`;
const pipelineId = 108;
- const createComponent = ({ mockData = {} } = {}) => {
+ const createComponent = ({ mockArtifacts = artifacts } = {}) => {
wrapper = shallowMount(PipelineArtifacts, {
- provide: {
- artifactsEndpoint,
- artifactsEndpointPlaceholder,
- },
propsData: {
pipelineId,
- },
- data() {
- return {
- ...mockData,
- };
+ artifacts: mockArtifacts,
},
stubs: {
GlSprintf,
@@ -45,80 +29,33 @@ describe('Pipelines Artifacts dropdown', () => {
});
};
- const findAlert = () => wrapper.findComponent(GlAlert);
const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findFirstGlDropdownItem = () => wrapper.find(GlDropdownItem);
const findAllGlDropdownItems = () => wrapper.find(GlDropdown).findAll(GlDropdownItem);
- beforeEach(() => {
- mockAxios = new MockAdapter(axios);
- });
-
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
- it('should render the dropdown', () => {
- createComponent();
-
- expect(findDropdown().exists()).toBe(true);
- });
-
- it('should fetch artifacts on dropdown click', async () => {
- const endpoint = artifactsEndpoint.replace(artifactsEndpointPlaceholder, pipelineId);
- mockAxios.onGet(endpoint).replyOnce(200, { artifacts });
- createComponent();
- findDropdown().vm.$emit('show');
- await waitForPromises();
-
- expect(mockAxios.history.get).toHaveLength(1);
- expect(wrapper.vm.artifacts).toEqual(artifacts);
- });
-
it('should render a dropdown with all the provided artifacts', () => {
- createComponent({ mockData: { artifacts } });
+ createComponent();
expect(findAllGlDropdownItems()).toHaveLength(artifacts.length);
});
it('should render a link with the provided path', () => {
- createComponent({ mockData: { artifacts } });
+ createComponent();
expect(findFirstGlDropdownItem().attributes('href')).toBe(artifacts[0].path);
expect(findFirstGlDropdownItem().text()).toBe(artifacts[0].name);
});
- describe('with a failing request', () => {
- it('should render an error message', async () => {
- const endpoint = artifactsEndpoint.replace(artifactsEndpointPlaceholder, pipelineId);
- mockAxios.onGet(endpoint).replyOnce(500);
- createComponent();
- findDropdown().vm.$emit('show');
- await waitForPromises();
-
- const error = findAlert();
- expect(error.exists()).toBe(true);
- expect(error.text()).toBe(i18n.artifactsFetchErrorMessage);
- });
- });
-
- describe('with no artifacts received', () => {
- it('should render empty alert message', () => {
- createComponent({ mockData: { artifacts: [] } });
-
- const emptyAlert = findAlert();
- expect(emptyAlert.exists()).toBe(true);
- expect(emptyAlert.text()).toBe(i18n.noArtifacts);
- });
- });
-
- describe('when artifacts are loading', () => {
- it('should show loading icon', () => {
- createComponent({ mockData: { isLoading: true } });
+ describe('with no artifacts', () => {
+ it('should not render the dropdown', () => {
+ createComponent({ mockArtifacts: [] });
- expect(findLoadingIcon().exists()).toBe(true);
+ expect(findDropdown().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js
index 2875498bb52..c024730570c 100644
--- a/spec/frontend/pipelines/pipelines_spec.js
+++ b/spec/frontend/pipelines/pipelines_spec.js
@@ -554,7 +554,7 @@ describe('Pipelines', () => {
});
it('renders the CI/CD templates', () => {
- expect(wrapper.find(PipelinesCiTemplates)).toExist();
+ expect(wrapper.findComponent(PipelinesCiTemplates).exists()).toBe(true);
});
describe('when the code_quality_walkthrough experiment is active', () => {
@@ -568,7 +568,7 @@ describe('Pipelines', () => {
});
it('renders the CI/CD templates', () => {
- expect(wrapper.find(PipelinesCiTemplates)).toExist();
+ expect(wrapper.findComponent(PipelinesCiTemplates).exists()).toBe(true);
});
});
@@ -597,7 +597,7 @@ describe('Pipelines', () => {
});
it('renders the CI/CD templates', () => {
- expect(wrapper.find(PipelinesCiTemplates)).toExist();
+ expect(wrapper.findComponent(PipelinesCiTemplates).exists()).toBe(true);
});
});
diff --git a/spec/frontend/pipelines/pipelines_table_spec.js b/spec/frontend/pipelines/pipelines_table_spec.js
index fb019b463b1..6fdbe907aed 100644
--- a/spec/frontend/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/pipelines/pipelines_table_spec.js
@@ -1,5 +1,5 @@
import '~/commons';
-import { GlTable } from '@gitlab/ui';
+import { GlTableLite } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import fixture from 'test_fixtures/pipelines/pipelines.json';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
@@ -44,7 +44,7 @@ describe('Pipelines Table', () => {
);
};
- const findGlTable = () => wrapper.findComponent(GlTable);
+ const findGlTableLite = () => wrapper.findComponent(GlTableLite);
const findStatusBadge = () => wrapper.findComponent(CiBadge);
const findPipelineInfo = () => wrapper.findComponent(PipelineUrl);
const findTriggerer = () => wrapper.findComponent(PipelineTriggerer);
@@ -77,7 +77,7 @@ describe('Pipelines Table', () => {
});
it('displays table', () => {
- expect(findGlTable().exists()).toBe(true);
+ expect(findGlTableLite().exists()).toBe(true);
});
it('should render table head with correct columns', () => {
diff --git a/spec/frontend/projects/commit/components/form_modal_spec.js b/spec/frontend/projects/commit/components/form_modal_spec.js
index 0c8089430d0..93e2ae13628 100644
--- a/spec/frontend/projects/commit/components/form_modal_spec.js
+++ b/spec/frontend/projects/commit/components/form_modal_spec.js
@@ -3,6 +3,7 @@ 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 api from '~/api';
import axios from '~/lib/utils/axios_utils';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import BranchesDropdown from '~/projects/commit/components/branches_dropdown.vue';
@@ -12,6 +13,8 @@ import eventHub from '~/projects/commit/event_hub';
import createStore from '~/projects/commit/store';
import mockData from '../mock_data';
+jest.mock('~/api');
+
describe('CommitFormModal', () => {
let wrapper;
let store;
@@ -167,4 +170,16 @@ describe('CommitFormModal', () => {
expect(findTargetProject().attributes('value')).toBe('_changed_project_value_');
});
});
+
+ it('action primary button triggers Redis HLL tracking api call', async () => {
+ createComponent(mount, {}, {}, { primaryActionEventName: 'test_event' });
+
+ await wrapper.vm.$nextTick();
+
+ jest.spyOn(findForm().element, 'submit');
+
+ getByText(mockData.modalPropsData.i18n.actionPrimaryText).trigger('click');
+
+ expect(api.trackRedisHllUserEvent).toHaveBeenCalledWith('test_event');
+ });
});
diff --git a/spec/frontend/projects/commits/components/author_select_spec.js b/spec/frontend/projects/commits/components/author_select_spec.js
index 9a8f7ff7582..60d36597fda 100644
--- a/spec/frontend/projects/commits/components/author_select_spec.js
+++ b/spec/frontend/projects/commits/components/author_select_spec.js
@@ -115,7 +115,7 @@ describe('Author Select', () => {
});
it('does not have popover text by default', () => {
- expect(wrapper.attributes('title')).not.toExist();
+ expect(wrapper.attributes('title')).toBeUndefined();
});
});
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 c255fcce321..e1e1aac09aa 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
@@ -52,9 +52,44 @@ exports[`Project remove modal initialized matches the snapshot 1`] = `
title="You are about to permanently delete this project"
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."
- />
+ <p>
+ This project is
+ <strong>
+ NOT
+ </strong>
+ a fork, and has the following:
+ </p>
+
+ <ul>
+ <li>
+ 1 issue
+ </li>
+
+ <li>
+ 2 merge requests
+ </li>
+
+ <li>
+ 3 forks
+ </li>
+
+ <li>
+ 4 stars
+ </li>
+ </ul>
+ After a project is permanently deleted, it
+ <strong>
+ cannot be recovered
+ </strong>
+ . Permanently deleting this project will
+ <strong>
+ immediately delete
+ </strong>
+ its repositories and
+ <strong>
+ all related resources
+ </strong>
+ , including issues, merge requests etc.
</gl-alert-stub>
<p
diff --git a/spec/frontend/projects/components/project_delete_button_spec.js b/spec/frontend/projects/components/project_delete_button_spec.js
index 444e465ebaa..bb6021fadda 100644
--- a/spec/frontend/projects/components/project_delete_button_spec.js
+++ b/spec/frontend/projects/components/project_delete_button_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { GlSprintf } from '@gitlab/ui';
import ProjectDeleteButton from '~/projects/components/project_delete_button.vue';
import SharedDeleteButton from '~/projects/components/shared/delete_button.vue';
@@ -12,6 +13,11 @@ describe('Project remove modal', () => {
const defaultProps = {
confirmPhrase: 'foo',
formPath: 'some/path',
+ isFork: false,
+ issuesCount: 1,
+ mergeRequestsCount: 2,
+ forksCount: 3,
+ starsCount: 4,
};
const createComponent = (props = {}) => {
@@ -21,6 +27,7 @@ describe('Project remove modal', () => {
...props,
},
stubs: {
+ GlSprintf,
SharedDeleteButton,
},
});
@@ -41,7 +48,10 @@ describe('Project remove modal', () => {
});
it('passes confirmPhrase and formPath props to the shared delete button', () => {
- expect(findSharedDeleteButton().props()).toEqual(defaultProps);
+ expect(findSharedDeleteButton().props()).toEqual({
+ confirmPhrase: defaultProps.confirmPhrase,
+ formPath: defaultProps.formPath,
+ });
});
});
});
diff --git a/spec/frontend/projects/details/upload_button_spec.js b/spec/frontend/projects/details/upload_button_spec.js
index ebb2b499ead..d7308963088 100644
--- a/spec/frontend/projects/details/upload_button_spec.js
+++ b/spec/frontend/projects/details/upload_button_spec.js
@@ -1,11 +1,8 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import UploadButton from '~/projects/details/upload_button.vue';
-import { trackFileUploadEvent } from '~/projects/upload_file_experiment_tracking';
import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
-jest.mock('~/projects/upload_file_experiment_tracking');
-
const MODAL_ID = 'details-modal-upload-blob';
describe('UploadButton', () => {
@@ -50,10 +47,6 @@ describe('UploadButton', () => {
wrapper.find(GlButton).vm.$emit('click');
});
- it('tracks the click_upload_modal_trigger event', () => {
- expect(trackFileUploadEvent).toHaveBeenCalledWith('click_upload_modal_trigger');
- });
-
it('opens the modal', () => {
expect(glModalDirective).toHaveBeenCalledWith(MODAL_ID);
});
diff --git a/spec/frontend/projects/new/components/new_project_url_select_spec.js b/spec/frontend/projects/new/components/new_project_url_select_spec.js
index aa16b71172b..b3f177a1f12 100644
--- a/spec/frontend/projects/new/components/new_project_url_select_spec.js
+++ b/spec/frontend/projects/new/components/new_project_url_select_spec.js
@@ -24,14 +24,23 @@ describe('NewProjectUrlSelect component', () => {
{
id: 'gid://gitlab/Group/26',
fullPath: 'flightjs',
+ name: 'Flight JS',
+ visibility: 'public',
+ webUrl: 'http://127.0.0.1:3000/flightjs',
},
{
id: 'gid://gitlab/Group/28',
fullPath: 'h5bp',
+ name: 'H5BP',
+ visibility: 'public',
+ webUrl: 'http://127.0.0.1:3000/h5bp',
},
{
id: 'gid://gitlab/Group/30',
fullPath: 'h5bp/subgroup',
+ name: 'H5BP Subgroup',
+ visibility: 'private',
+ webUrl: 'http://127.0.0.1:3000/h5bp/subgroup',
},
],
},
@@ -79,6 +88,10 @@ describe('NewProjectUrlSelect component', () => {
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findInput = () => wrapper.findComponent(GlSearchBoxByType);
const findHiddenInput = () => wrapper.find('input');
+ const clickDropdownItem = async () => {
+ wrapper.findComponent(GlDropdownItem).vm.$emit('click');
+ await wrapper.vm.$nextTick();
+ };
afterEach(() => {
wrapper.destroy();
@@ -127,7 +140,6 @@ describe('NewProjectUrlSelect component', () => {
it('focuses on the input when the dropdown is opened', async () => {
wrapper = mountComponent({ mountFn: mount });
-
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
@@ -140,7 +152,6 @@ describe('NewProjectUrlSelect component', () => {
it('renders expected dropdown items', async () => {
wrapper = mountComponent({ mountFn: mount });
-
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
@@ -160,7 +171,6 @@ describe('NewProjectUrlSelect component', () => {
beforeEach(async () => {
wrapper = mountComponent({ mountFn: mount });
-
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
@@ -195,23 +205,38 @@ describe('NewProjectUrlSelect component', () => {
};
wrapper = mountComponent({ search: 'no matches', queryResponse, mountFn: mount });
-
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
expect(wrapper.find('li').text()).toBe('No matches found');
});
- it('updates hidden input with selected namespace', async () => {
+ it('emits `update-visibility` event to update the visibility radio options', async () => {
wrapper = mountComponent();
-
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
- wrapper.findComponent(GlDropdownItem).vm.$emit('click');
+ const spy = jest.spyOn(eventHub, '$emit');
+ await clickDropdownItem();
+
+ const namespace = data.currentUser.groups.nodes[0];
+
+ expect(spy).toHaveBeenCalledWith('update-visibility', {
+ name: namespace.name,
+ visibility: namespace.visibility,
+ showPath: namespace.webUrl,
+ editPath: `${namespace.webUrl}/-/edit`,
+ });
+ });
+
+ it('updates hidden input with selected namespace', async () => {
+ wrapper = mountComponent();
+ jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
+ await clickDropdownItem();
+
expect(findHiddenInput().attributes()).toMatchObject({
name: 'project[namespace_id]',
value: getIdFromGraphQLId(data.currentUser.groups.nodes[0].id).toString(),
diff --git a/spec/frontend/projects/pipelines/charts/components/app_spec.js b/spec/frontend/projects/pipelines/charts/components/app_spec.js
index 987a215eb4c..b4067f6a72b 100644
--- a/spec/frontend/projects/pipelines/charts/components/app_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/app_spec.js
@@ -11,6 +11,7 @@ jest.mock('~/lib/utils/url_utility');
const DeploymentFrequencyChartsStub = { name: 'DeploymentFrequencyCharts', render: () => {} };
const LeadTimeChartsStub = { name: 'LeadTimeCharts', render: () => {} };
+const ProjectQualitySummaryStub = { name: 'ProjectQualitySummary', render: () => {} };
describe('ProjectsPipelinesChartsApp', () => {
let wrapper;
@@ -23,10 +24,12 @@ describe('ProjectsPipelinesChartsApp', () => {
{
provide: {
shouldRenderDoraCharts: true,
+ shouldRenderQualitySummary: true,
},
stubs: {
DeploymentFrequencyCharts: DeploymentFrequencyChartsStub,
LeadTimeCharts: LeadTimeChartsStub,
+ ProjectQualitySummary: ProjectQualitySummaryStub,
},
},
mountOptions,
@@ -44,6 +47,7 @@ describe('ProjectsPipelinesChartsApp', () => {
const findLeadTimeCharts = () => wrapper.find(LeadTimeChartsStub);
const findDeploymentFrequencyCharts = () => wrapper.find(DeploymentFrequencyChartsStub);
const findPipelineCharts = () => wrapper.find(PipelineCharts);
+ const findProjectQualitySummary = () => wrapper.find(ProjectQualitySummaryStub);
describe('when all charts are available', () => {
beforeEach(() => {
@@ -70,6 +74,10 @@ describe('ProjectsPipelinesChartsApp', () => {
expect(findLeadTimeCharts().exists()).toBe(true);
});
+ it('renders the project quality summary', () => {
+ expect(findProjectQualitySummary().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`);
@@ -163,9 +171,11 @@ describe('ProjectsPipelinesChartsApp', () => {
});
});
- describe('when the dora charts are not available', () => {
+ describe('when the dora charts are not available and project quality summary is not available', () => {
beforeEach(() => {
- createComponent({ provide: { shouldRenderDoraCharts: false } });
+ createComponent({
+ provide: { shouldRenderDoraCharts: false, shouldRenderQualitySummary: false },
+ });
});
it('does not render tabs', () => {
@@ -176,4 +186,14 @@ describe('ProjectsPipelinesChartsApp', () => {
expect(findPipelineCharts().exists()).toBe(true);
});
});
+
+ describe('when the project quality summary is not available', () => {
+ beforeEach(() => {
+ createComponent({ provide: { shouldRenderQualitySummary: false } });
+ });
+
+ it('does not render the tab', () => {
+ expect(findProjectQualitySummary().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/projects/projects_filterable_list_spec.js b/spec/frontend/projects/projects_filterable_list_spec.js
index d4dbf85b5ca..a41e8b7bc09 100644
--- a/spec/frontend/projects/projects_filterable_list_spec.js
+++ b/spec/frontend/projects/projects_filterable_list_spec.js
@@ -1,5 +1,4 @@
-// eslint-disable-next-line import/no-deprecated
-import { getJSONFixture, setHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture } from 'helpers/fixtures';
import ProjectsFilterableList from '~/projects/projects_filterable_list';
describe('ProjectsFilterableList', () => {
@@ -15,8 +14,6 @@ describe('ProjectsFilterableList', () => {
</div>
<div class="js-projects-list-holder"></div>
`);
- // eslint-disable-next-line import/no-deprecated
- getJSONFixture('static/projects.json');
form = document.querySelector('form#project-filter-form');
filter = document.querySelector('.js-projects-list-filter');
holder = document.querySelector('.js-projects-list-holder');
diff --git a/spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js b/spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js
new file mode 100644
index 00000000000..dbea94cbd53
--- /dev/null
+++ b/spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js
@@ -0,0 +1,98 @@
+import { GlTokenSelector, GlToken } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import TopicsTokenSelector from '~/projects/settings/topics/components/topics_token_selector.vue';
+
+const mockTopics = [
+ { id: 1, name: 'topic1', avatarUrl: 'avatar.com/topic1.png' },
+ { id: 2, name: 'GitLab', avatarUrl: 'avatar.com/GitLab.png' },
+];
+
+describe('TopicsTokenSelector', () => {
+ let wrapper;
+ let div;
+ let input;
+
+ const createComponent = (selected) => {
+ wrapper = mount(TopicsTokenSelector, {
+ attachTo: div,
+ propsData: {
+ selected,
+ },
+ data() {
+ return {
+ topics: mockTopics,
+ };
+ },
+ mocks: {
+ $apollo: {
+ queries: {
+ topics: { loading: false },
+ },
+ },
+ },
+ });
+ };
+
+ const findTokenSelector = () => wrapper.findComponent(GlTokenSelector);
+
+ const findTokenSelectorInput = () => findTokenSelector().find('input[type="text"]');
+
+ const setTokenSelectorInputValue = (value) => {
+ const tokenSelectorInput = findTokenSelectorInput();
+
+ tokenSelectorInput.element.value = value;
+ tokenSelectorInput.trigger('input');
+
+ return nextTick();
+ };
+
+ const tokenSelectorTriggerEnter = (event) => {
+ const tokenSelectorInput = findTokenSelectorInput();
+ tokenSelectorInput.trigger('keydown.enter', event);
+ };
+
+ beforeEach(() => {
+ div = document.createElement('div');
+ input = document.createElement('input');
+ input.setAttribute('type', 'text');
+ input.id = 'project_topic_list_field';
+ document.body.appendChild(div);
+ document.body.appendChild(input);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ div.remove();
+ input.remove();
+ });
+
+ describe('when component is mounted', () => {
+ it('parses selected into tokens', async () => {
+ const selected = [
+ { id: 11, name: 'topic1' },
+ { id: 12, name: 'topic2' },
+ { id: 13, name: 'topic3' },
+ ];
+ createComponent(selected);
+ await nextTick();
+
+ wrapper.findAllComponents(GlToken).wrappers.forEach((tokenWrapper, index) => {
+ expect(tokenWrapper.text()).toBe(selected[index].name);
+ });
+ });
+ });
+
+ describe('when enter key is pressed', () => {
+ it('does not submit the form if token selector text input has a value', async () => {
+ createComponent();
+
+ await setTokenSelectorInputValue('topic');
+
+ const event = { preventDefault: jest.fn() };
+ tokenSelectorTriggerEnter(event);
+
+ expect(event.preventDefault).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/projects/settings_service_desk/components/mock_data.js b/spec/frontend/projects/settings_service_desk/components/mock_data.js
new file mode 100644
index 00000000000..934778ff601
--- /dev/null
+++ b/spec/frontend/projects/settings_service_desk/components/mock_data.js
@@ -0,0 +1,8 @@
+export const TEMPLATES = [
+ 'Project #1',
+ [
+ { name: 'Bug', project_id: 1 },
+ { name: 'Documentation', project_id: 1 },
+ { name: 'Security release', project_id: 1 },
+ ],
+];
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 8acf2376860..62224612387 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
@@ -21,6 +21,7 @@ describe('ServiceDeskRoot', () => {
outgoingName: 'GitLab Support Bot',
projectKey: 'key',
selectedTemplate: 'Bug',
+ selectedFileTemplateProjectId: 42,
templates: ['Bug', 'Documentation'],
};
@@ -52,6 +53,7 @@ describe('ServiceDeskRoot', () => {
initialOutgoingName: provideData.outgoingName,
initialProjectKey: provideData.projectKey,
initialSelectedTemplate: provideData.selectedTemplate,
+ initialSelectedFileTemplateProjectId: provideData.selectedFileTemplateProjectId,
isEnabled: provideData.initialIsEnabled,
isTemplateSaving: false,
templates: provideData.templates,
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 eacf858f22c..0fd3e7446da 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,4 +1,4 @@
-import { GlButton, GlFormSelect, GlLoadingIcon, GlToggle } from '@gitlab/ui';
+import { GlButton, GlDropdown, GlLoadingIcon, GlToggle } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
@@ -13,7 +13,7 @@ describe('ServiceDeskSetting', () => {
const findIncomingEmail = () => wrapper.findByTestId('incoming-email');
const findIncomingEmailLabel = () => wrapper.findByTestId('incoming-email-describer');
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
- const findTemplateDropdown = () => wrapper.find(GlFormSelect);
+ const findTemplateDropdown = () => wrapper.find(GlDropdown);
const findToggle = () => wrapper.find(GlToggle);
const createComponent = ({ props = {}, mountFunction = shallowMount } = {}) =>
@@ -128,6 +128,23 @@ describe('ServiceDeskSetting', () => {
expect(input.exists()).toBe(true);
expect(input.attributes('disabled')).toBeUndefined();
});
+
+ it('shows error when value contains uppercase or special chars', async () => {
+ wrapper = createComponent({
+ props: { customEmailEnabled: true },
+ mountFunction: mount,
+ });
+
+ const input = wrapper.findByTestId('project-suffix');
+
+ input.setValue('abc_A.');
+ input.trigger('blur');
+
+ await wrapper.vm.$nextTick();
+
+ const errorText = wrapper.find('.text-danger');
+ expect(errorText.exists()).toBe(true);
+ });
});
describe('customEmail is the same as incomingEmail', () => {
@@ -144,63 +161,6 @@ describe('ServiceDeskSetting', () => {
});
});
});
-
- describe('templates dropdown', () => {
- it('renders a dropdown to choose a template', () => {
- wrapper = createComponent();
-
- expect(findTemplateDropdown().exists()).toBe(true);
- });
-
- it('renders a dropdown with a default value of ""', () => {
- wrapper = createComponent({ mountFunction: mount });
-
- expect(findTemplateDropdown().element.value).toEqual('');
- });
-
- it('renders a dropdown with a value of "Bug" when it is the initial value', () => {
- const templates = ['Bug', 'Documentation', 'Security release'];
-
- 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 = createComponent({
- props: { templates: [] },
- mountFunction: mount,
- });
-
- // The dropdown by default has one empty option
- expect(findTemplateDropdown().element.children).toHaveLength(1);
- });
-
- it('renders a dropdown with options when the project has templates', () => {
- const templates = ['Bug', 'Documentation', 'Security release'];
-
- wrapper = createComponent({
- props: { templates },
- mountFunction: mount,
- });
-
- // An empty-named template is prepended so the user can select no template
- const expectedTemplates = [''].concat(templates);
-
- const dropdown = findTemplateDropdown();
- const dropdownList = Array.from(dropdown.element.children).map(
- (option) => option.innerText,
- );
-
- expect(dropdown.element.children).toHaveLength(expectedTemplates.length);
- expect(dropdownList.includes('Bug')).toEqual(true);
- expect(dropdownList.includes('Documentation')).toEqual(true);
- expect(dropdownList.includes('Security release')).toEqual(true);
- });
- });
});
describe('save button', () => {
@@ -214,6 +174,7 @@ describe('ServiceDeskSetting', () => {
wrapper = createComponent({
props: {
initialSelectedTemplate: 'Bug',
+ initialSelectedFileTemplateProjectId: 42,
initialOutgoingName: 'GitLab Support Bot',
initialProjectKey: 'key',
},
@@ -225,6 +186,7 @@ describe('ServiceDeskSetting', () => {
const payload = {
selectedTemplate: 'Bug',
+ fileTemplateProjectId: 42,
outgoingName: 'GitLab Support Bot',
projectKey: 'key',
};
diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_template_dropdown_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_template_dropdown_spec.js
new file mode 100644
index 00000000000..cdb355f5a9b
--- /dev/null
+++ b/spec/frontend/projects/settings_service_desk/components/service_desk_template_dropdown_spec.js
@@ -0,0 +1,80 @@
+import { GlDropdown, GlDropdownSectionHeader, GlDropdownItem } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import ServiceDeskTemplateDropdown from '~/projects/settings_service_desk/components/service_desk_setting.vue';
+import { TEMPLATES } from './mock_data';
+
+describe('ServiceDeskTemplateDropdown', () => {
+ let wrapper;
+
+ const findTemplateDropdown = () => wrapper.find(GlDropdown);
+
+ const createComponent = ({ props = {} } = {}) =>
+ extendedWrapper(
+ mount(ServiceDeskTemplateDropdown, {
+ propsData: {
+ isEnabled: true,
+ ...props,
+ },
+ }),
+ );
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ }
+ });
+
+ describe('templates dropdown', () => {
+ it('renders a dropdown to choose a template', () => {
+ wrapper = createComponent();
+
+ expect(findTemplateDropdown().exists()).toBe(true);
+ });
+
+ it('renders a dropdown with a default value of "Choose a template"', () => {
+ wrapper = createComponent();
+
+ expect(findTemplateDropdown().props('text')).toEqual('Choose a template');
+ });
+
+ it('renders a dropdown with a value of "Bug" when it is the initial value', () => {
+ const templates = TEMPLATES;
+
+ wrapper = createComponent({
+ props: { initialSelectedTemplate: 'Bug', initialSelectedTemplateProjectId: 1, templates },
+ });
+
+ expect(findTemplateDropdown().props('text')).toEqual('Bug');
+ });
+
+ it('renders a dropdown with header items', () => {
+ wrapper = createComponent({
+ props: { templates: TEMPLATES },
+ });
+
+ const headerItems = wrapper.findAll(GlDropdownSectionHeader);
+
+ expect(headerItems).toHaveLength(1);
+ expect(headerItems.at(0).text()).toBe(TEMPLATES[0]);
+ });
+
+ it('renders a dropdown with options when the project has templates', () => {
+ const templates = TEMPLATES;
+
+ wrapper = createComponent({
+ props: { templates },
+ });
+
+ const expectedTemplates = templates[1];
+
+ const items = wrapper.findAll(GlDropdownItem);
+ const dropdownList = expectedTemplates.map((_, index) => items.at(index).text());
+
+ expect(items).toHaveLength(expectedTemplates.length);
+ expect(dropdownList.includes('Bug')).toEqual(true);
+ expect(dropdownList.includes('Documentation')).toEqual(true);
+ expect(dropdownList.includes('Security release')).toEqual(true);
+ });
+ });
+});
diff --git a/spec/frontend/projects/storage_counter/components/storage_table_spec.js b/spec/frontend/projects/storage_counter/components/storage_table_spec.js
index 14298318fff..c9e56d8f033 100644
--- a/spec/frontend/projects/storage_counter/components/storage_table_spec.js
+++ b/spec/frontend/projects/storage_counter/components/storage_table_spec.js
@@ -1,4 +1,4 @@
-import { GlTable } from '@gitlab/ui';
+import { GlTableLite } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import StorageTable from '~/projects/storage_counter/components/storage_table.vue';
@@ -22,7 +22,7 @@ describe('StorageTable', () => {
);
};
- const findTable = () => wrapper.findComponent(GlTable);
+ const findTable = () => wrapper.findComponent(GlTableLite);
beforeEach(() => {
createComponent();
@@ -37,6 +37,7 @@ describe('StorageTable', () => {
({ storageType: { id, name, description } }) => {
expect(wrapper.findByTestId(`${id}-name`).text()).toBe(name);
expect(wrapper.findByTestId(`${id}-description`).text()).toBe(description);
+ expect(wrapper.findByTestId(`${id}-icon`).props('name')).toBe(id);
expect(wrapper.findByTestId(`${id}-help-link`).attributes('href')).toBe(
defaultProvideValues.helpLinks[id.replace(`Size`, `HelpPagePath`)]
.replace(`Size`, ``)
diff --git a/spec/frontend/projects/storage_counter/components/storage_type_icon_spec.js b/spec/frontend/projects/storage_counter/components/storage_type_icon_spec.js
new file mode 100644
index 00000000000..01efd6f14bd
--- /dev/null
+++ b/spec/frontend/projects/storage_counter/components/storage_type_icon_spec.js
@@ -0,0 +1,41 @@
+import { mount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
+import StorageTypeIcon from '~/projects/storage_counter/components/storage_type_icon.vue';
+
+describe('StorageTypeIcon', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = mount(StorageTypeIcon, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ const findGlIcon = () => wrapper.findComponent(GlIcon);
+
+ describe('rendering icon', () => {
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it.each`
+ expected | provided
+ ${'doc-image'} | ${'lfsObjectsSize'}
+ ${'snippet'} | ${'snippetsSize'}
+ ${'infrastructure-registry'} | ${'repositorySize'}
+ ${'package'} | ${'packagesSize'}
+ ${'upload'} | ${'uploadsSize'}
+ ${'disk'} | ${'wikiSize'}
+ ${'disk'} | ${'anything-else'}
+ `(
+ 'renders icon with name of $expected when name prop is $provided',
+ ({ expected, provided }) => {
+ createComponent({ name: provided });
+
+ expect(findGlIcon().props('name')).toBe(expected);
+ },
+ );
+ });
+});
diff --git a/spec/frontend/projects/storage_counter/mock_data.js b/spec/frontend/projects/storage_counter/mock_data.js
index b9fa68b3ec7..6b3e23ac386 100644
--- a/spec/frontend/projects/storage_counter/mock_data.js
+++ b/spec/frontend/projects/storage_counter/mock_data.js
@@ -1,23 +1,6 @@
-export const mockGetProjectStorageCountGraphQLResponse = {
- data: {
- project: {
- id: 'gid://gitlab/Project/20',
- statistics: {
- buildArtifactsSize: 400000.0,
- pipelineArtifactsSize: 25000.0,
- lfsObjectsSize: 4800000.0,
- packagesSize: 3800000.0,
- repositorySize: 3900000.0,
- snippetsSize: 1200000.0,
- storageSize: 15300000.0,
- uploadsSize: 900000.0,
- wikiSize: 300000.0,
- __typename: 'ProjectStatistics',
- },
- __typename: 'Project',
- },
- },
-};
+import mockGetProjectStorageCountGraphQLResponse from 'test_fixtures/graphql/projects/storage_counter/project_storage.query.graphql.json';
+
+export { mockGetProjectStorageCountGraphQLResponse };
export const mockEmptyResponse = { data: { project: null } };
@@ -37,7 +20,7 @@ export const defaultProvideValues = {
export const projectData = {
storage: {
- totalUsage: '14.6 MiB',
+ totalUsage: '13.8 MiB',
storageTypes: [
{
storageType: {
@@ -45,7 +28,7 @@ export const projectData = {
name: 'Artifacts',
description: 'Pipeline artifacts and job artifacts, created with CI/CD.',
warningMessage:
- 'There is a known issue with Artifact storage where the total could be incorrect for some projects. More details and progress are available in %{warningLinkStart}the epic%{warningLinkEnd}.',
+ 'Because of a known issue, the artifact total for some projects may be incorrect. For more details, read %{warningLinkStart}the epic%{warningLinkEnd}.',
helpPath: '/build-artifacts',
},
value: 400000,
@@ -53,7 +36,7 @@ export const projectData = {
{
storageType: {
id: 'lfsObjectsSize',
- name: 'LFS Storage',
+ name: 'LFS storage',
description: 'Audio samples, videos, datasets, and graphics.',
helpPath: '/lsf-objects',
},
@@ -72,7 +55,7 @@ export const projectData = {
storageType: {
id: 'repositorySize',
name: 'Repository',
- description: 'Git repository, managed by the Gitaly service.',
+ description: 'Git repository.',
helpPath: '/repository',
},
value: 3900000,
@@ -84,7 +67,7 @@ export const projectData = {
description: 'Shared bits of code and text.',
helpPath: '/snippets',
},
- value: 1200000,
+ value: 0,
},
{
storageType: {
diff --git a/spec/frontend/projects/storage_counter/utils_spec.js b/spec/frontend/projects/storage_counter/utils_spec.js
index 57c755266a0..fb91975a3cf 100644
--- a/spec/frontend/projects/storage_counter/utils_spec.js
+++ b/spec/frontend/projects/storage_counter/utils_spec.js
@@ -14,4 +14,21 @@ describe('parseGetProjectStorageResults', () => {
),
).toMatchObject(projectData);
});
+
+ it('includes storage type with size of 0 in returned value', () => {
+ const mockedResponse = mockGetProjectStorageCountGraphQLResponse.data;
+ // ensuring a specific storage type item has size of 0
+ mockedResponse.project.statistics.repositorySize = 0;
+
+ const response = parseGetProjectStorageResults(mockedResponse, defaultProvideValues.helpLinks);
+
+ expect(response.storage.storageTypes).toEqual(
+ expect.arrayContaining([
+ {
+ storageType: expect.any(Object),
+ value: 0,
+ },
+ ]),
+ );
+ });
});
diff --git a/spec/frontend/projects/upload_file_experiment_tracking_spec.js b/spec/frontend/projects/upload_file_experiment_tracking_spec.js
deleted file mode 100644
index 6817529e07e..00000000000
--- a/spec/frontend/projects/upload_file_experiment_tracking_spec.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import ExperimentTracking from '~/experimentation/experiment_tracking';
-import { trackFileUploadEvent } from '~/projects/upload_file_experiment_tracking';
-
-jest.mock('~/experimentation/experiment_tracking');
-
-const eventName = 'click_upload_modal_form_submit';
-const fixture = `<a class='js-upload-file-experiment-trigger'></a><div class='project-home-panel empty-project'></div>`;
-
-beforeEach(() => {
- document.body.innerHTML = fixture;
-});
-
-afterEach(() => {
- document.body.innerHTML = '';
-});
-
-describe('trackFileUploadEvent', () => {
- it('initializes ExperimentTracking with the correct tracking event', () => {
- trackFileUploadEvent(eventName);
-
- expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(eventName);
- });
-
- it('calls ExperimentTracking with the correct arguments', () => {
- trackFileUploadEvent(eventName);
-
- expect(ExperimentTracking).toHaveBeenCalledWith('empty_repo_upload', {
- label: 'blob-upload-modal',
- property: 'empty',
- });
- });
-
- it('calls ExperimentTracking with the correct arguments when the project is not empty', () => {
- document.querySelector('.empty-project').remove();
-
- trackFileUploadEvent(eventName);
-
- expect(ExperimentTracking).toHaveBeenCalledWith('empty_repo_upload', {
- label: 'blob-upload-modal',
- property: 'nonempty',
- });
- });
-});
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 67f62815720..486fb699275 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
@@ -66,8 +66,8 @@ describe('RelatedMergeRequests', () => {
describe('template', () => {
it('should render related merge request items', () => {
- expect(wrapper.find('.js-items-count').text()).toEqual('2');
- expect(wrapper.findAll(RelatedIssuableItem).length).toEqual(2);
+ expect(wrapper.find('[data-testid="count"]').text()).toBe('2');
+ expect(wrapper.findAll(RelatedIssuableItem)).toHaveLength(2);
const props = wrapper.findAll(RelatedIssuableItem).at(1).props();
const data = mockData[1];
diff --git a/spec/frontend/releases/components/tag_field_new_spec.js b/spec/frontend/releases/components/tag_field_new_spec.js
index 114e46ce64b..0f416e46dba 100644
--- a/spec/frontend/releases/components/tag_field_new_spec.js
+++ b/spec/frontend/releases/components/tag_field_new_spec.js
@@ -1,6 +1,7 @@
import { GlDropdownItem } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import Vue from 'vue';
+import { __ } from '~/locale';
import TagFieldNew from '~/releases/components/tag_field_new.vue';
import createStore from '~/releases/stores';
import createEditNewModule from '~/releases/stores/modules/edit_new';
@@ -84,7 +85,8 @@ describe('releases/components/tag_field_new', () => {
beforeEach(() => createComponent());
it('renders a label', () => {
- expect(findTagNameFormGroup().attributes().label).toBe('Tag name');
+ expect(findTagNameFormGroup().attributes().label).toBe(__('Tag name'));
+ expect(findTagNameFormGroup().props().labelDescription).toBe(__('*Required'));
});
describe('when the user selects a new tag name', () => {
diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js
index 59db537282b..d40e97bf5a3 100644
--- a/spec/frontend/repository/components/blob_content_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_content_viewer_spec.js
@@ -1,5 +1,5 @@
import { GlLoadingIcon } from '@gitlab/ui';
-import { shallowMount, mount, createLocalVue } from '@vue/test-utils';
+import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
@@ -19,6 +19,15 @@ import TextViewer from '~/repository/components/blob_viewers/text_viewer.vue';
import blobInfoQuery from '~/repository/queries/blob_info.query.graphql';
import { redirectTo } from '~/lib/utils/url_utility';
import { isLoggedIn } from '~/lib/utils/common_utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import {
+ simpleViewerMock,
+ richViewerMock,
+ projectMock,
+ userPermissionsMock,
+ propsMock,
+ refMock,
+} from '../mock_data';
jest.mock('~/repository/components/blob_viewers');
jest.mock('~/lib/utils/url_utility');
@@ -27,151 +36,63 @@ jest.mock('~/lib/utils/common_utils');
let wrapper;
let mockResolver;
-const simpleMockData = {
- name: 'some_file.js',
- size: 123,
- rawSize: 123,
- rawTextBlob: 'raw content',
- type: 'text',
- fileType: 'text',
- tooLarge: false,
- path: 'some_file.js',
- webPath: 'some_file.js',
- editBlobPath: 'some_file.js/edit',
- ideEditPath: 'some_file.js/ide/edit',
- forkAndEditPath: 'some_file.js/fork/edit',
- ideForkAndEditPath: 'some_file.js/fork/ide',
- canModifyBlob: true,
- storedExternally: false,
- rawPath: 'some_file.js',
- externalStorageUrl: 'some_file.js',
- replacePath: 'some_file.js/replace',
- deletePath: 'some_file.js/delete',
- simpleViewer: {
- fileType: 'text',
- tooLarge: false,
- type: 'simple',
- renderError: null,
- },
- richViewer: null,
-};
-const richMockData = {
- ...simpleMockData,
- richViewer: {
- fileType: 'markup',
- tooLarge: false,
- type: 'rich',
- renderError: null,
- },
-};
-
-const projectMockData = {
- userPermissions: {
- pushCode: true,
- downloadCode: true,
- createMergeRequestIn: true,
- forkProject: true,
- },
- repository: {
- empty: false,
- },
-};
-
const localVue = createLocalVue();
const mockAxios = new MockAdapter(axios);
-const createComponentWithApollo = (mockData = {}, inject = {}) => {
+const createComponent = async (mockData = {}, mountFn = shallowMount) => {
localVue.use(VueApollo);
- const defaultPushCode = projectMockData.userPermissions.pushCode;
- const defaultDownloadCode = projectMockData.userPermissions.downloadCode;
- const defaultEmptyRepo = projectMockData.repository.empty;
const {
- blobs,
- emptyRepo = defaultEmptyRepo,
- canPushCode = defaultPushCode,
- canDownloadCode = defaultDownloadCode,
- createMergeRequestIn = projectMockData.userPermissions.createMergeRequestIn,
- forkProject = projectMockData.userPermissions.forkProject,
- pathLocks = [],
+ blob = simpleViewerMock,
+ empty = projectMock.repository.empty,
+ pushCode = userPermissionsMock.pushCode,
+ forkProject = userPermissionsMock.forkProject,
+ downloadCode = userPermissionsMock.downloadCode,
+ createMergeRequestIn = userPermissionsMock.createMergeRequestIn,
+ isBinary,
+ inject = {},
} = mockData;
- mockResolver = jest.fn().mockResolvedValue({
- data: {
- project: {
- id: '1234',
- userPermissions: {
- pushCode: canPushCode,
- downloadCode: canDownloadCode,
- createMergeRequestIn,
- forkProject,
- },
- pathLocks: {
- nodes: pathLocks,
- },
- repository: {
- empty: emptyRepo,
- blobs: {
- nodes: [blobs],
- },
- },
- },
+ const project = {
+ ...projectMock,
+ userPermissions: {
+ pushCode,
+ forkProject,
+ downloadCode,
+ createMergeRequestIn,
+ },
+ repository: {
+ empty,
+ blobs: { nodes: [blob] },
},
+ };
+
+ mockResolver = jest.fn().mockResolvedValue({
+ data: { isBinary, project },
});
const fakeApollo = createMockApollo([[blobInfoQuery, mockResolver]]);
- wrapper = shallowMount(BlobContentViewer, {
- localVue,
- apolloProvider: fakeApollo,
- propsData: {
- path: 'some_file.js',
- projectPath: 'some/path',
- },
- mixins: [
- {
- data: () => ({ ref: 'default-ref' }),
- },
- ],
- provide: {
- ...inject,
- },
- });
-};
+ wrapper = extendedWrapper(
+ mountFn(BlobContentViewer, {
+ localVue,
+ apolloProvider: fakeApollo,
+ propsData: propsMock,
+ mixins: [{ data: () => ({ ref: refMock }) }],
+ provide: { ...inject },
+ }),
+ );
-const createFactory = (mountFn) => (
- { props = {}, mockData = {}, stubs = {} } = {},
- loading = false,
-) => {
- wrapper = mountFn(BlobContentViewer, {
- propsData: {
- path: 'some_file.js',
- projectPath: 'some/path',
- ...props,
- },
- mocks: {
- $apollo: {
- queries: {
- project: {
- loading,
- refetch: jest.fn(),
- },
- },
- },
- },
- stubs,
- });
+ wrapper.setData({ project, isBinary });
- wrapper.setData(mockData);
+ await waitForPromises();
};
-const factory = createFactory(shallowMount);
-const fullFactory = createFactory(mount);
-
describe('Blob content viewer component', () => {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findBlobHeader = () => wrapper.findComponent(BlobHeader);
const findBlobEdit = () => wrapper.findComponent(BlobEdit);
+ const findPipelineEditor = () => wrapper.findByTestId('pipeline-editor');
const findBlobContent = () => wrapper.findComponent(BlobContent);
const findBlobButtonGroup = () => wrapper.findComponent(BlobButtonGroup);
const findForkSuggestion = () => wrapper.findComponent(ForkSuggestion);
@@ -187,25 +108,24 @@ describe('Blob content viewer component', () => {
});
it('renders a GlLoadingIcon component', () => {
- factory({ mockData: { blobInfo: simpleMockData } }, true);
+ createComponent();
expect(findLoadingIcon().exists()).toBe(true);
});
describe('simple viewer', () => {
- beforeEach(() => {
- factory({ mockData: { blobInfo: simpleMockData } });
- });
+ it('renders a BlobHeader component', async () => {
+ await createComponent();
- it('renders a BlobHeader component', () => {
expect(findBlobHeader().props('activeViewerType')).toEqual('simple');
expect(findBlobHeader().props('hasRenderError')).toEqual(false);
expect(findBlobHeader().props('hideViewerSwitcher')).toEqual(true);
- expect(findBlobHeader().props('blob')).toEqual(simpleMockData);
+ expect(findBlobHeader().props('blob')).toEqual(simpleViewerMock);
});
- it('renders a BlobContent component', () => {
- expect(findBlobContent().props('loading')).toEqual(false);
+ it('renders a BlobContent component', async () => {
+ await createComponent();
+
expect(findBlobContent().props('isRawContent')).toBe(true);
expect(findBlobContent().props('activeViewer')).toEqual({
fileType: 'text',
@@ -217,8 +137,7 @@ describe('Blob content viewer component', () => {
describe('legacy viewers', () => {
it('loads a legacy viewer when a viewer component is not available', async () => {
- createComponentWithApollo({ blobs: { ...simpleMockData, fileType: 'unknown' } });
- await waitForPromises();
+ await createComponent({ blob: { ...simpleViewerMock, fileType: 'unknown' } });
expect(mockAxios.history.get).toHaveLength(1);
expect(mockAxios.history.get[0].url).toEqual('some_file.js?format=json&viewer=simple');
@@ -227,21 +146,18 @@ describe('Blob content viewer component', () => {
});
describe('rich viewer', () => {
- beforeEach(() => {
- factory({
- mockData: { blobInfo: richMockData, activeViewerType: 'rich' },
- });
- });
+ it('renders a BlobHeader component', async () => {
+ await createComponent({ blob: richViewerMock });
- it('renders a BlobHeader component', () => {
expect(findBlobHeader().props('activeViewerType')).toEqual('rich');
expect(findBlobHeader().props('hasRenderError')).toEqual(false);
expect(findBlobHeader().props('hideViewerSwitcher')).toEqual(false);
- expect(findBlobHeader().props('blob')).toEqual(richMockData);
+ expect(findBlobHeader().props('blob')).toEqual(richViewerMock);
});
- it('renders a BlobContent component', () => {
- expect(findBlobContent().props('loading')).toEqual(false);
+ it('renders a BlobContent component', async () => {
+ await createComponent({ blob: richViewerMock });
+
expect(findBlobContent().props('isRawContent')).toBe(true);
expect(findBlobContent().props('activeViewer')).toEqual({
fileType: 'markup',
@@ -252,6 +168,8 @@ describe('Blob content viewer component', () => {
});
it('updates viewer type when viewer changed is clicked', async () => {
+ await createComponent({ blob: richViewerMock });
+
expect(findBlobContent().props('activeViewer')).toEqual(
expect.objectContaining({
type: 'rich',
@@ -273,8 +191,7 @@ describe('Blob content viewer component', () => {
describe('legacy viewers', () => {
it('loads a legacy viewer when a viewer component is not available', async () => {
- createComponentWithApollo({ blobs: { ...richMockData, fileType: 'unknown' } });
- await waitForPromises();
+ await createComponent({ blob: { ...richViewerMock, fileType: 'unknown' } });
expect(mockAxios.history.get).toHaveLength(1);
expect(mockAxios.history.get[0].url).toEqual('some_file.js?format=json&viewer=rich');
@@ -287,9 +204,9 @@ describe('Blob content viewer component', () => {
viewerProps.mockRestore();
});
- it('does not render a BlobContent component if a Blob viewer is available', () => {
- loadViewer.mockReturnValueOnce(() => true);
- factory({ mockData: { blobInfo: richMockData } });
+ it('does not render a BlobContent component if a Blob viewer is available', async () => {
+ loadViewer.mockReturnValue(() => true);
+ await createComponent({ blob: richViewerMock });
expect(findBlobContent().exists()).toBe(false);
});
@@ -305,15 +222,13 @@ describe('Blob content viewer component', () => {
loadViewer.mockReturnValue(loadViewerReturnValue);
viewerProps.mockReturnValue(viewerPropsReturnValue);
- factory({
- mockData: {
- blobInfo: {
- ...simpleMockData,
- fileType: null,
- simpleViewer: {
- ...simpleMockData.simpleViewer,
- fileType: viewer,
- },
+ createComponent({
+ blob: {
+ ...simpleViewerMock,
+ fileType: 'null',
+ simpleViewer: {
+ ...simpleViewerMock.simpleViewer,
+ fileType: viewer,
},
},
});
@@ -327,18 +242,10 @@ describe('Blob content viewer component', () => {
});
describe('BlobHeader action slot', () => {
- const { ideEditPath, editBlobPath } = simpleMockData;
+ const { ideEditPath, editBlobPath } = simpleViewerMock;
it('renders BlobHeaderEdit buttons in simple viewer', async () => {
- fullFactory({
- mockData: { blobInfo: simpleMockData },
- stubs: {
- BlobContent: true,
- BlobReplace: true,
- },
- });
-
- await nextTick();
+ await createComponent({ inject: { BlobContent: true, BlobReplace: true } }, mount);
expect(findBlobEdit().props()).toMatchObject({
editPath: editBlobPath,
@@ -348,15 +255,7 @@ describe('Blob content viewer component', () => {
});
it('renders BlobHeaderEdit button in rich viewer', async () => {
- fullFactory({
- mockData: { blobInfo: richMockData },
- stubs: {
- BlobContent: true,
- BlobReplace: true,
- },
- });
-
- await nextTick();
+ await createComponent({ blob: richViewerMock }, mount);
expect(findBlobEdit().props()).toMatchObject({
editPath: editBlobPath,
@@ -366,15 +265,7 @@ describe('Blob content viewer component', () => {
});
it('renders BlobHeaderEdit button for binary files', async () => {
- fullFactory({
- mockData: { blobInfo: richMockData, isBinary: true },
- stubs: {
- BlobContent: true,
- BlobReplace: true,
- },
- });
-
- await nextTick();
+ await createComponent({ blob: richViewerMock, isBinary: true }, mount);
expect(findBlobEdit().props()).toMatchObject({
editPath: editBlobPath,
@@ -383,42 +274,36 @@ describe('Blob content viewer component', () => {
});
});
- describe('blob header binary file', () => {
- it.each([richMockData, { simpleViewer: { fileType: 'download' } }])(
- 'passes the correct isBinary value when viewing a binary file',
- async (blobInfo) => {
- fullFactory({
- mockData: {
- blobInfo,
- isBinary: true,
- },
- stubs: { BlobContent: true, BlobReplace: true },
- });
+ it('renders Pipeline Editor button for .gitlab-ci files', async () => {
+ const pipelineEditorPath = 'some/path/.gitlab-ce';
+ const blob = { ...simpleViewerMock, pipelineEditorPath };
+ await createComponent({ blob, inject: { BlobContent: true, BlobReplace: true } }, mount);
- await nextTick();
+ expect(findPipelineEditor().exists()).toBe(true);
+ expect(findPipelineEditor().attributes('href')).toBe(pipelineEditorPath);
+ });
- expect(findBlobHeader().props('isBinary')).toBe(true);
- },
- );
+ describe('blob header binary file', () => {
+ it('passes the correct isBinary value when viewing a binary file', async () => {
+ await createComponent({ blob: richViewerMock, isBinary: true });
+
+ expect(findBlobHeader().props('isBinary')).toBe(true);
+ });
it('passes the correct header props when viewing a non-text file', async () => {
- fullFactory({
- mockData: {
- blobInfo: {
- ...simpleMockData,
+ await createComponent(
+ {
+ blob: {
+ ...simpleViewerMock,
simpleViewer: {
- ...simpleMockData.simpleViewer,
+ ...simpleViewerMock.simpleViewer,
fileType: 'image',
},
},
+ isBinary: true,
},
- stubs: {
- BlobContent: true,
- BlobReplace: true,
- },
- });
-
- await nextTick();
+ mount,
+ );
expect(findBlobHeader().props('hideViewerSwitcher')).toBe(true);
expect(findBlobHeader().props('isBinary')).toBe(true);
@@ -427,27 +312,16 @@ describe('Blob content viewer component', () => {
});
describe('BlobButtonGroup', () => {
- const { name, path, replacePath, webPath } = simpleMockData;
+ const { name, path, replacePath, webPath } = simpleViewerMock;
const {
userPermissions: { pushCode, downloadCode },
repository: { empty },
- } = projectMockData;
+ } = projectMock;
it('renders component', async () => {
window.gon.current_user_id = 1;
- fullFactory({
- mockData: {
- blobInfo: simpleMockData,
- project: { userPermissions: { pushCode, downloadCode }, repository: { empty } },
- },
- stubs: {
- BlobContent: true,
- BlobButtonGroup: true,
- },
- });
-
- await nextTick();
+ await createComponent({ pushCode, downloadCode, empty }, mount);
expect(findBlobButtonGroup().props()).toMatchObject({
name,
@@ -467,21 +341,14 @@ describe('Blob content viewer component', () => {
${false} | ${true} | ${false}
${true} | ${false} | ${false}
`('passes the correct lock states', async ({ canPushCode, canDownloadCode, canLock }) => {
- fullFactory({
- mockData: {
- blobInfo: simpleMockData,
- project: {
- userPermissions: { pushCode: canPushCode, downloadCode: canDownloadCode },
- repository: { empty },
- },
+ await createComponent(
+ {
+ pushCode: canPushCode,
+ downloadCode: canDownloadCode,
+ empty,
},
- stubs: {
- BlobContent: true,
- BlobButtonGroup: true,
- },
- });
-
- await nextTick();
+ mount,
+ );
expect(findBlobButtonGroup().props('canLock')).toBe(canLock);
});
@@ -489,15 +356,7 @@ describe('Blob content viewer component', () => {
it('does not render if not logged in', async () => {
isLoggedIn.mockReturnValueOnce(false);
- fullFactory({
- mockData: { blobInfo: simpleMockData },
- stubs: {
- BlobContent: true,
- BlobReplace: true,
- },
- });
-
- await nextTick();
+ await createComponent();
expect(findBlobButtonGroup().exists()).toBe(false);
});
@@ -506,10 +365,7 @@ describe('Blob content viewer component', () => {
describe('blob info query', () => {
it('is called with originalBranch value if the prop has a value', async () => {
- const inject = { originalBranch: 'some-branch' };
- createComponentWithApollo({ blobs: simpleMockData }, inject);
-
- await waitForPromises();
+ await createComponent({ inject: { originalBranch: 'some-branch' } });
expect(mockResolver).toHaveBeenCalledWith(
expect.objectContaining({
@@ -519,10 +375,7 @@ describe('Blob content viewer component', () => {
});
it('is called with ref value if the originalBranch prop has no value', async () => {
- const inject = { originalBranch: null };
- createComponentWithApollo({ blobs: simpleMockData }, inject);
-
- await waitForPromises();
+ await createComponent();
expect(mockResolver).toHaveBeenCalledWith(
expect.objectContaining({
@@ -533,24 +386,16 @@ describe('Blob content viewer component', () => {
});
describe('edit blob', () => {
- beforeEach(() => {
- fullFactory({
- mockData: { blobInfo: simpleMockData },
- stubs: {
- BlobContent: true,
- BlobReplace: true,
- },
- });
- });
+ beforeEach(() => createComponent({}, mount));
it('simple edit redirects to the simple editor', () => {
findBlobEdit().vm.$emit('edit', 'simple');
- expect(redirectTo).toHaveBeenCalledWith(simpleMockData.editBlobPath);
+ expect(redirectTo).toHaveBeenCalledWith(simpleViewerMock.editBlobPath);
});
it('IDE edit redirects to the IDE editor', () => {
findBlobEdit().vm.$emit('edit', 'ide');
- expect(redirectTo).toHaveBeenCalledWith(simpleMockData.ideEditPath);
+ expect(redirectTo).toHaveBeenCalledWith(simpleViewerMock.ideEditPath);
});
it.each`
@@ -569,16 +414,14 @@ describe('Blob content viewer component', () => {
showForkSuggestion,
}) => {
isLoggedIn.mockReturnValueOnce(loggedIn);
- fullFactory({
- mockData: {
- blobInfo: { ...simpleMockData, canModifyBlob },
- project: { userPermissions: { createMergeRequestIn, forkProject } },
+ await createComponent(
+ {
+ blob: { ...simpleViewerMock, canModifyBlob },
+ createMergeRequestIn,
+ forkProject,
},
- stubs: {
- BlobContent: true,
- BlobButtonGroup: true,
- },
- });
+ mount,
+ );
findBlobEdit().vm.$emit('edit', 'simple');
await nextTick();
diff --git a/spec/frontend/repository/components/upload_blob_modal_spec.js b/spec/frontend/repository/components/upload_blob_modal_spec.js
index 08a6583b60c..36847107558 100644
--- a/spec/frontend/repository/components/upload_blob_modal_spec.js
+++ b/spec/frontend/repository/components/upload_blob_modal_spec.js
@@ -6,11 +6,9 @@ import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
-import { trackFileUploadEvent } from '~/projects/upload_file_experiment_tracking';
import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
-jest.mock('~/projects/upload_file_experiment_tracking');
jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
@@ -162,10 +160,6 @@ describe('UploadBlobModal', () => {
await waitForPromises();
});
- it('tracks the click_upload_modal_trigger event when opening the modal', () => {
- expect(trackFileUploadEvent).toHaveBeenCalledWith('click_upload_modal_form_submit');
- });
-
it('redirects to the uploaded file', () => {
expect(visitUrl).toHaveBeenCalled();
});
@@ -185,10 +179,6 @@ describe('UploadBlobModal', () => {
await waitForPromises();
});
- it('does not track an event', () => {
- expect(trackFileUploadEvent).not.toHaveBeenCalled();
- });
-
it('creates a flash error', () => {
expect(createFlash).toHaveBeenCalledWith({
message: 'Error uploading file. Please try again.',
diff --git a/spec/frontend/repository/mock_data.js b/spec/frontend/repository/mock_data.js
new file mode 100644
index 00000000000..adf5991ac3c
--- /dev/null
+++ b/spec/frontend/repository/mock_data.js
@@ -0,0 +1,57 @@
+export const simpleViewerMock = {
+ name: 'some_file.js',
+ size: 123,
+ rawSize: 123,
+ rawTextBlob: 'raw content',
+ fileType: 'text',
+ path: 'some_file.js',
+ webPath: 'some_file.js',
+ editBlobPath: 'some_file.js/edit',
+ ideEditPath: 'some_file.js/ide/edit',
+ forkAndEditPath: 'some_file.js/fork/edit',
+ ideForkAndEditPath: 'some_file.js/fork/ide',
+ canModifyBlob: true,
+ storedExternally: false,
+ rawPath: 'some_file.js',
+ replacePath: 'some_file.js/replace',
+ pipelineEditorPath: '',
+ simpleViewer: {
+ fileType: 'text',
+ tooLarge: false,
+ type: 'simple',
+ renderError: null,
+ },
+ richViewer: null,
+};
+
+export const richViewerMock = {
+ ...simpleViewerMock,
+ richViewer: {
+ fileType: 'markup',
+ tooLarge: false,
+ type: 'rich',
+ renderError: null,
+ },
+};
+
+export const userPermissionsMock = {
+ pushCode: true,
+ forkProject: true,
+ downloadCode: true,
+ createMergeRequestIn: true,
+};
+
+export const projectMock = {
+ id: '1234',
+ userPermissions: userPermissionsMock,
+ pathLocks: {
+ nodes: [],
+ },
+ repository: {
+ empty: false,
+ },
+};
+
+export const propsMock = { path: 'some_file.js', projectPath: 'some/path' };
+
+export const refMock = 'default-ref';
diff --git a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
index 33e9c122080..7eda9aa2850 100644
--- a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
+++ b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
@@ -10,9 +10,10 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory } from '~/lib/utils/url_utility';
import AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue';
+import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
import RunnerList from '~/runner/components/runner_list.vue';
-import RunnerManualSetupHelp from '~/runner/components/runner_manual_setup_help.vue';
+import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
import RunnerPagination from '~/runner/components/runner_pagination.vue';
import {
@@ -22,7 +23,6 @@ import {
DEFAULT_SORT,
INSTANCE_TYPE,
PARAM_KEY_STATUS,
- PARAM_KEY_RUNNER_TYPE,
PARAM_KEY_TAG,
STATUS_ACTIVE,
RUNNER_PAGE_SIZE,
@@ -34,7 +34,11 @@ import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered
import { runnersData, runnersDataPaginated } from '../mock_data';
const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
-const mockActiveRunnersCount = 2;
+const mockActiveRunnersCount = '2';
+const mockAllRunnersCount = '6';
+const mockInstanceRunnersCount = '3';
+const mockGroupRunnersCount = '2';
+const mockProjectRunnersCount = '1';
jest.mock('~/flash');
jest.mock('~/runner/sentry_utils');
@@ -50,7 +54,8 @@ describe('AdminRunnersApp', () => {
let wrapper;
let mockRunnersQuery;
- const findRunnerManualSetupHelp = () => wrapper.findComponent(RunnerManualSetupHelp);
+ const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
+ const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs);
const findRunnerList = () => wrapper.findComponent(RunnerList);
const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
const findRunnerPaginationPrev = () =>
@@ -66,8 +71,12 @@ describe('AdminRunnersApp', () => {
localVue,
apolloProvider: createMockApollo(handlers),
propsData: {
- activeRunnersCount: mockActiveRunnersCount,
registrationToken: mockRegistrationToken,
+ activeRunnersCount: mockActiveRunnersCount,
+ allRunnersCount: mockAllRunnersCount,
+ instanceRunnersCount: mockInstanceRunnersCount,
+ groupRunnersCount: mockGroupRunnersCount,
+ projectRunnersCount: mockProjectRunnersCount,
...props,
},
});
@@ -86,8 +95,19 @@ describe('AdminRunnersApp', () => {
wrapper.destroy();
});
+ it('shows the runner tabs with a runner count', async () => {
+ createComponent({ mountFn: mount });
+
+ await waitForPromises();
+
+ expect(findRunnerTypeTabs().text()).toMatchInterpolatedText(
+ `All ${mockAllRunnersCount} Instance ${mockInstanceRunnersCount} Group ${mockGroupRunnersCount} Project ${mockProjectRunnersCount}`,
+ );
+ });
+
it('shows the runner setup instructions', () => {
- expect(findRunnerManualSetupHelp().props('registrationToken')).toBe(mockRegistrationToken);
+ expect(findRegistrationDropdown().props('registrationToken')).toBe(mockRegistrationToken);
+ expect(findRegistrationDropdown().props('type')).toBe(INSTANCE_TYPE);
});
it('shows the runners list', () => {
@@ -126,10 +146,6 @@ describe('AdminRunnersApp', () => {
options: expect.any(Array),
}),
expect.objectContaining({
- type: PARAM_KEY_RUNNER_TYPE,
- options: expect.any(Array),
- }),
- expect.objectContaining({
type: PARAM_KEY_TAG,
recentTokenValuesStorageKey: `${ADMIN_FILTERED_SEARCH_NAMESPACE}-recent-tags`,
}),
@@ -154,9 +170,9 @@ describe('AdminRunnersApp', () => {
it('sets the filters in the search bar', () => {
expect(findRunnerFilteredSearchBar().props('value')).toEqual({
+ runnerType: INSTANCE_TYPE,
filters: [
{ type: 'status', value: { data: STATUS_ACTIVE, operator: '=' } },
- { type: 'runner_type', value: { data: INSTANCE_TYPE, operator: '=' } },
{ type: 'tag', value: { data: 'tag1', operator: '=' } },
],
sort: 'CREATED_DESC',
@@ -178,6 +194,7 @@ describe('AdminRunnersApp', () => {
describe('when a filter is selected by the user', () => {
beforeEach(() => {
findRunnerFilteredSearchBar().vm.$emit('input', {
+ runnerType: null,
filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ACTIVE, operator: '=' } }],
sort: CREATED_ASC,
});
diff --git a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
index 5aa3879ac3e..2874bdbe280 100644
--- a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
+++ b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
@@ -8,12 +8,11 @@ import RunnerActionCell from '~/runner/components/cells/runner_actions_cell.vue'
import getGroupRunnersQuery from '~/runner/graphql/get_group_runners.query.graphql';
import getRunnersQuery from '~/runner/graphql/get_runners.query.graphql';
import runnerDeleteMutation from '~/runner/graphql/runner_delete.mutation.graphql';
-import runnerUpdateMutation from '~/runner/graphql/runner_update.mutation.graphql';
+import runnerActionsUpdateMutation from '~/runner/graphql/runner_actions_update.mutation.graphql';
import { captureException } from '~/runner/sentry_utils';
-import { runnersData, runnerData } from '../../mock_data';
+import { runnersData } from '../../mock_data';
const mockRunner = runnersData.data.runners.nodes[0];
-const mockRunnerDetails = runnerData.data.runner;
const getRunnersQueryName = getRunnersQuery.definitions[0].name.value;
const getGroupRunnersQueryName = getGroupRunnersQuery.definitions[0].name.value;
@@ -27,7 +26,7 @@ jest.mock('~/runner/sentry_utils');
describe('RunnerTypeCell', () => {
let wrapper;
const runnerDeleteMutationHandler = jest.fn();
- const runnerUpdateMutationHandler = jest.fn();
+ const runnerActionsUpdateMutationHandler = jest.fn();
const findEditBtn = () => wrapper.findByTestId('edit-runner');
const findToggleActiveBtn = () => wrapper.findByTestId('toggle-active-runner');
@@ -46,7 +45,7 @@ describe('RunnerTypeCell', () => {
localVue,
apolloProvider: createMockApollo([
[runnerDeleteMutation, runnerDeleteMutationHandler],
- [runnerUpdateMutation, runnerUpdateMutationHandler],
+ [runnerActionsUpdateMutation, runnerActionsUpdateMutationHandler],
]),
...options,
}),
@@ -62,10 +61,10 @@ describe('RunnerTypeCell', () => {
},
});
- runnerUpdateMutationHandler.mockResolvedValue({
+ runnerActionsUpdateMutationHandler.mockResolvedValue({
data: {
runnerUpdate: {
- runner: mockRunnerDetails,
+ runner: mockRunner,
errors: [],
},
},
@@ -74,7 +73,7 @@ describe('RunnerTypeCell', () => {
afterEach(() => {
runnerDeleteMutationHandler.mockReset();
- runnerUpdateMutationHandler.mockReset();
+ runnerActionsUpdateMutationHandler.mockReset();
wrapper.destroy();
});
@@ -116,12 +115,12 @@ describe('RunnerTypeCell', () => {
describe(`When clicking on the ${icon} button`, () => {
it(`The apollo mutation to set active to ${newActiveValue} is called`, async () => {
- expect(runnerUpdateMutationHandler).toHaveBeenCalledTimes(0);
+ expect(runnerActionsUpdateMutationHandler).toHaveBeenCalledTimes(0);
await findToggleActiveBtn().vm.$emit('click');
- expect(runnerUpdateMutationHandler).toHaveBeenCalledTimes(1);
- expect(runnerUpdateMutationHandler).toHaveBeenCalledWith({
+ expect(runnerActionsUpdateMutationHandler).toHaveBeenCalledTimes(1);
+ expect(runnerActionsUpdateMutationHandler).toHaveBeenCalledWith({
input: {
id: mockRunner.id,
active: newActiveValue,
@@ -145,7 +144,7 @@ describe('RunnerTypeCell', () => {
const mockErrorMsg = 'Update error!';
beforeEach(async () => {
- runnerUpdateMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
+ runnerActionsUpdateMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
await findToggleActiveBtn().vm.$emit('click');
});
@@ -167,10 +166,10 @@ describe('RunnerTypeCell', () => {
const mockErrorMsg2 = 'User not allowed!';
beforeEach(async () => {
- runnerUpdateMutationHandler.mockResolvedValue({
+ runnerActionsUpdateMutationHandler.mockResolvedValue({
data: {
runnerUpdate: {
- runner: runnerData.data.runner,
+ runner: mockRunner,
errors: [mockErrorMsg, mockErrorMsg2],
},
},
diff --git a/spec/frontend/runner/components/cells/runner_status_cell_spec.js b/spec/frontend/runner/components/cells/runner_status_cell_spec.js
new file mode 100644
index 00000000000..20a1cdf7236
--- /dev/null
+++ b/spec/frontend/runner/components/cells/runner_status_cell_spec.js
@@ -0,0 +1,69 @@
+import { GlBadge } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import RunnerStatusCell from '~/runner/components/cells/runner_status_cell.vue';
+import { INSTANCE_TYPE, STATUS_ONLINE, STATUS_OFFLINE } from '~/runner/constants';
+
+describe('RunnerTypeCell', () => {
+ let wrapper;
+
+ const findBadgeAt = (i) => wrapper.findAllComponents(GlBadge).at(i);
+
+ const createComponent = ({ runner = {} } = {}) => {
+ wrapper = mount(RunnerStatusCell, {
+ propsData: {
+ runner: {
+ runnerType: INSTANCE_TYPE,
+ active: true,
+ status: STATUS_ONLINE,
+ ...runner,
+ },
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('Displays online status', () => {
+ createComponent();
+
+ expect(wrapper.text()).toMatchInterpolatedText('online');
+ expect(findBadgeAt(0).text()).toBe('online');
+ });
+
+ it('Displays offline status', () => {
+ createComponent({
+ runner: {
+ status: STATUS_OFFLINE,
+ },
+ });
+
+ expect(wrapper.text()).toMatchInterpolatedText('offline');
+ expect(findBadgeAt(0).text()).toBe('offline');
+ });
+
+ it('Displays paused status', () => {
+ createComponent({
+ runner: {
+ active: false,
+ status: STATUS_ONLINE,
+ },
+ });
+
+ expect(wrapper.text()).toMatchInterpolatedText('online paused');
+
+ expect(findBadgeAt(0).text()).toBe('online');
+ expect(findBadgeAt(1).text()).toBe('paused');
+ });
+
+ it('Is empty when data is missing', () => {
+ createComponent({
+ runner: {
+ status: null,
+ },
+ });
+
+ expect(wrapper.text()).toBe('');
+ });
+});
diff --git a/spec/frontend/runner/components/cells/runner_summary_cell_spec.js b/spec/frontend/runner/components/cells/runner_summary_cell_spec.js
index 1c9282e0acd..b6d957d27ea 100644
--- a/spec/frontend/runner/components/cells/runner_summary_cell_spec.js
+++ b/spec/frontend/runner/components/cells/runner_summary_cell_spec.js
@@ -1,5 +1,6 @@
-import { mount } from '@vue/test-utils';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import RunnerSummaryCell from '~/runner/components/cells/runner_summary_cell.vue';
+import { INSTANCE_TYPE, PROJECT_TYPE } from '~/runner/constants';
const mockId = '1';
const mockShortSha = '2P6oDVDm';
@@ -8,13 +9,17 @@ const mockDescription = 'runner-1';
describe('RunnerTypeCell', () => {
let wrapper;
- const createComponent = (options) => {
- wrapper = mount(RunnerSummaryCell, {
+ const findLockIcon = () => wrapper.findByTestId('lock-icon');
+
+ const createComponent = (runner, options) => {
+ wrapper = mountExtended(RunnerSummaryCell, {
propsData: {
runner: {
id: `gid://gitlab/Ci::Runner/${mockId}`,
shortSha: mockShortSha,
description: mockDescription,
+ runnerType: INSTANCE_TYPE,
+ ...runner,
},
},
...options,
@@ -33,6 +38,23 @@ describe('RunnerTypeCell', () => {
expect(wrapper.text()).toContain(`#${mockId} (${mockShortSha})`);
});
+ it('Displays the runner type', () => {
+ expect(wrapper.text()).toContain('shared');
+ });
+
+ it('Does not display the locked icon', () => {
+ expect(findLockIcon().exists()).toBe(false);
+ });
+
+ it('Displays the locked icon for locked runners', () => {
+ createComponent({
+ runnerType: PROJECT_TYPE,
+ locked: true,
+ });
+
+ expect(findLockIcon().exists()).toBe(true);
+ });
+
it('Displays the runner description', () => {
expect(wrapper.text()).toContain(mockDescription);
});
@@ -40,11 +62,14 @@ describe('RunnerTypeCell', () => {
it('Displays a custom slot', () => {
const slotContent = 'My custom runner summary';
- createComponent({
- slots: {
- 'runner-name': slotContent,
+ createComponent(
+ {},
+ {
+ slots: {
+ 'runner-name': slotContent,
+ },
},
- });
+ );
expect(wrapper.text()).toContain(slotContent);
});
diff --git a/spec/frontend/runner/components/cells/runner_type_cell_spec.js b/spec/frontend/runner/components/cells/runner_type_cell_spec.js
deleted file mode 100644
index 48958a282fc..00000000000
--- a/spec/frontend/runner/components/cells/runner_type_cell_spec.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import { GlBadge } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import RunnerTypeCell from '~/runner/components/cells/runner_type_cell.vue';
-import { INSTANCE_TYPE } from '~/runner/constants';
-
-describe('RunnerTypeCell', () => {
- let wrapper;
-
- const findBadges = () => wrapper.findAllComponents(GlBadge);
-
- const createComponent = ({ runner = {} } = {}) => {
- wrapper = mount(RunnerTypeCell, {
- propsData: {
- runner: {
- runnerType: INSTANCE_TYPE,
- active: true,
- locked: false,
- ...runner,
- },
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('Displays the runner type', () => {
- createComponent();
-
- expect(findBadges()).toHaveLength(1);
- expect(findBadges().at(0).text()).toBe('shared');
- });
-
- it('Displays locked and paused states', () => {
- createComponent({
- runner: {
- active: false,
- locked: true,
- },
- });
-
- expect(findBadges()).toHaveLength(3);
- expect(findBadges().at(0).text()).toBe('shared');
- expect(findBadges().at(1).text()).toBe('locked');
- expect(findBadges().at(2).text()).toBe('paused');
- });
-});
diff --git a/spec/frontend/runner/components/helpers/masked_value_spec.js b/spec/frontend/runner/components/helpers/masked_value_spec.js
deleted file mode 100644
index f87315057ec..00000000000
--- a/spec/frontend/runner/components/helpers/masked_value_spec.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import { GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import MaskedValue from '~/runner/components/helpers/masked_value.vue';
-
-const mockSecret = '01234567890';
-const mockMasked = '***********';
-
-describe('MaskedValue', () => {
- let wrapper;
-
- const findButton = () => wrapper.findComponent(GlButton);
-
- const createComponent = ({ props = {} } = {}) => {
- wrapper = shallowMount(MaskedValue, {
- propsData: {
- value: mockSecret,
- ...props,
- },
- });
- };
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('Displays masked value by default', () => {
- expect(wrapper.text()).toBe(mockMasked);
- });
-
- describe('When the icon is clicked', () => {
- beforeEach(() => {
- findButton().vm.$emit('click');
- });
-
- it('Displays the actual value', () => {
- expect(wrapper.text()).toBe(mockSecret);
- expect(wrapper.text()).not.toBe(mockMasked);
- });
-
- it('When user clicks again, displays masked value', async () => {
- await findButton().vm.$emit('click');
-
- expect(wrapper.text()).toBe(mockMasked);
- expect(wrapper.text()).not.toBe(mockSecret);
- });
- });
-});
diff --git a/spec/frontend/runner/components/registration/registration_dropdown_spec.js b/spec/frontend/runner/components/registration/registration_dropdown_spec.js
new file mode 100644
index 00000000000..d18d2bec18e
--- /dev/null
+++ b/spec/frontend/runner/components/registration/registration_dropdown_spec.js
@@ -0,0 +1,169 @@
+import { GlDropdown, GlDropdownItem, GlDropdownForm } from '@gitlab/ui';
+import { createLocalVue, mount, shallowMount, createWrapper } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+
+import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
+import RegistrationTokenResetDropdownItem from '~/runner/components/registration/registration_token_reset_dropdown_item.vue';
+
+import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
+
+import getRunnerPlatformsQuery from '~/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql';
+import getRunnerSetupInstructionsQuery from '~/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql';
+
+import {
+ mockGraphqlRunnerPlatforms,
+ mockGraphqlInstructions,
+} from 'jest/vue_shared/components/runner_instructions/mock_data';
+
+const mockToken = '0123456789';
+const maskToken = '**********';
+
+describe('RegistrationDropdown', () => {
+ let wrapper;
+
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+
+ const findRegistrationInstructionsDropdownItem = () => wrapper.findComponent(GlDropdownItem);
+ const findTokenDropdownItem = () => wrapper.findComponent(GlDropdownForm);
+ const findTokenResetDropdownItem = () =>
+ wrapper.findComponent(RegistrationTokenResetDropdownItem);
+
+ const findToggleMaskButton = () => wrapper.findByTestId('toggle-masked');
+
+ const createComponent = ({ props = {}, ...options } = {}, mountFn = shallowMount) => {
+ wrapper = extendedWrapper(
+ mountFn(RegistrationDropdown, {
+ propsData: {
+ registrationToken: mockToken,
+ type: INSTANCE_TYPE,
+ ...props,
+ },
+ ...options,
+ }),
+ );
+ };
+
+ it.each`
+ type | text
+ ${INSTANCE_TYPE} | ${'Register an instance runner'}
+ ${GROUP_TYPE} | ${'Register a group runner'}
+ ${PROJECT_TYPE} | ${'Register a project runner'}
+ `('Dropdown text for type $type is "$text"', () => {
+ createComponent({ props: { type: INSTANCE_TYPE } }, mount);
+
+ expect(wrapper.text()).toContain('Register an instance runner');
+ });
+
+ it('Passes attributes to the dropdown component', () => {
+ createComponent({ attrs: { right: true } });
+
+ expect(findDropdown().attributes()).toMatchObject({ right: 'true' });
+ });
+
+ describe('Instructions dropdown item', () => {
+ it('Displays "Show runner" dropdown item', () => {
+ createComponent();
+
+ expect(findRegistrationInstructionsDropdownItem().text()).toBe(
+ 'Show runner installation and registration instructions',
+ );
+ });
+
+ describe('When the dropdown item is clicked', () => {
+ const localVue = createLocalVue();
+ localVue.use(VueApollo);
+
+ const requestHandlers = [
+ [getRunnerPlatformsQuery, jest.fn().mockResolvedValue(mockGraphqlRunnerPlatforms)],
+ [getRunnerSetupInstructionsQuery, jest.fn().mockResolvedValue(mockGraphqlInstructions)],
+ ];
+
+ const findModalInBody = () =>
+ createWrapper(document.body).find('[data-testid="runner-instructions-modal"]');
+
+ beforeEach(() => {
+ createComponent(
+ {
+ localVue,
+ // Mock load modal contents from API
+ apolloProvider: createMockApollo(requestHandlers),
+ // Use `attachTo` to find the modal
+ attachTo: document.body,
+ },
+ mount,
+ );
+
+ findRegistrationInstructionsDropdownItem().trigger('click');
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('opens the modal with contents', () => {
+ const modalText = findModalInBody()
+ .text()
+ .replace(/[\n\t\s]+/g, ' ');
+
+ expect(modalText).toContain('Install a runner');
+
+ // Environment selector
+ expect(modalText).toContain('Environment');
+ expect(modalText).toContain('Linux macOS Windows Docker Kubernetes');
+
+ // Architecture selector
+ expect(modalText).toContain('Architecture');
+ expect(modalText).toContain('amd64 amd64 386 arm arm64');
+
+ expect(modalText).toContain('Download and install binary');
+ });
+ });
+ });
+
+ describe('Registration token', () => {
+ it('Displays dropdown form for the registration token', () => {
+ createComponent();
+
+ expect(findTokenDropdownItem().exists()).toBe(true);
+ });
+
+ it('Displays masked value by default', () => {
+ createComponent({}, mount);
+
+ expect(findTokenDropdownItem().text()).toMatchInterpolatedText(
+ `Registration token ${maskToken}`,
+ );
+ });
+ });
+
+ describe('Reset token item', () => {
+ it('Displays registration token reset item', () => {
+ createComponent();
+
+ expect(findTokenResetDropdownItem().exists()).toBe(true);
+ });
+
+ it.each([INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE])('Set up token reset for %s', (type) => {
+ createComponent({ props: { type } });
+
+ expect(findTokenResetDropdownItem().props('type')).toBe(type);
+ });
+ });
+
+ it('Updates the token when it gets reset', async () => {
+ createComponent({}, mount);
+
+ const newToken = 'mock1';
+
+ findTokenResetDropdownItem().vm.$emit('tokenReset', newToken);
+ findToggleMaskButton().vm.$emit('click', { stopPropagation: jest.fn() });
+ await nextTick();
+
+ expect(findTokenDropdownItem().text()).toMatchInterpolatedText(
+ `Registration token ${newToken}`,
+ );
+ });
+});
diff --git a/spec/frontend/runner/components/runner_registration_token_reset_spec.js b/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js
index 8b360b88417..0d002c272b4 100644
--- a/spec/frontend/runner/components/runner_registration_token_reset_spec.js
+++ b/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js
@@ -1,11 +1,11 @@
-import { GlButton } from '@gitlab/ui';
+import { GlDropdownItem, GlLoadingIcon, GlToast } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash, { FLASH_TYPES } from '~/flash';
-import RunnerRegistrationTokenReset from '~/runner/components/runner_registration_token_reset.vue';
+import createFlash from '~/flash';
+import RegistrationTokenResetDropdownItem from '~/runner/components/registration/registration_token_reset_dropdown_item.vue';
import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
import runnersRegistrationTokenResetMutation from '~/runner/graphql/runners_registration_token_reset.mutation.graphql';
import { captureException } from '~/runner/sentry_utils';
@@ -15,17 +15,20 @@ jest.mock('~/runner/sentry_utils');
const localVue = createLocalVue();
localVue.use(VueApollo);
+localVue.use(GlToast);
const mockNewToken = 'NEW_TOKEN';
-describe('RunnerRegistrationTokenReset', () => {
+describe('RegistrationTokenResetDropdownItem', () => {
let wrapper;
let runnersRegistrationTokenResetMutationHandler;
+ let showToast;
- const findButton = () => wrapper.findComponent(GlButton);
+ const findDropdownItem = () => wrapper.findComponent(GlDropdownItem);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const createComponent = ({ props, provide = {} } = {}) => {
- wrapper = shallowMount(RunnerRegistrationTokenReset, {
+ wrapper = shallowMount(RegistrationTokenResetDropdownItem, {
localVue,
provide,
propsData: {
@@ -36,6 +39,8 @@ describe('RunnerRegistrationTokenReset', () => {
[runnersRegistrationTokenResetMutation, runnersRegistrationTokenResetMutationHandler],
]),
});
+
+ showToast = wrapper.vm.$toast ? jest.spyOn(wrapper.vm.$toast, 'show') : null;
};
beforeEach(() => {
@@ -58,7 +63,7 @@ describe('RunnerRegistrationTokenReset', () => {
});
it('Displays reset button', () => {
- expect(findButton().exists()).toBe(true);
+ expect(findDropdownItem().exists()).toBe(true);
});
describe('On click and confirmation', () => {
@@ -78,7 +83,8 @@ describe('RunnerRegistrationTokenReset', () => {
});
window.confirm.mockReturnValueOnce(true);
- findButton().vm.$emit('click');
+
+ findDropdownItem().trigger('click');
await waitForPromises();
});
@@ -95,14 +101,13 @@ describe('RunnerRegistrationTokenReset', () => {
});
it('does not show a loading state', () => {
- expect(findButton().props('loading')).toBe(false);
+ expect(findLoadingIcon().exists()).toBe(false);
});
it('shows confirmation', () => {
- expect(createFlash).toHaveBeenLastCalledWith({
- message: expect.stringContaining('registration token generated'),
- type: FLASH_TYPES.SUCCESS,
- });
+ expect(showToast).toHaveBeenLastCalledWith(
+ expect.stringContaining('registration token generated'),
+ );
});
});
});
@@ -110,7 +115,7 @@ describe('RunnerRegistrationTokenReset', () => {
describe('On click without confirmation', () => {
beforeEach(async () => {
window.confirm.mockReturnValueOnce(false);
- findButton().vm.$emit('click');
+ findDropdownItem().vm.$emit('click');
await waitForPromises();
});
@@ -123,11 +128,11 @@ describe('RunnerRegistrationTokenReset', () => {
});
it('does not show a loading state', () => {
- expect(findButton().props('loading')).toBe(false);
+ expect(findLoadingIcon().exists()).toBe(false);
});
it('does not shows confirmation', () => {
- expect(createFlash).not.toHaveBeenCalled();
+ expect(showToast).not.toHaveBeenCalled();
});
});
@@ -138,7 +143,7 @@ describe('RunnerRegistrationTokenReset', () => {
runnersRegistrationTokenResetMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
window.confirm.mockReturnValueOnce(true);
- findButton().vm.$emit('click');
+ findDropdownItem().trigger('click');
await waitForPromises();
expect(createFlash).toHaveBeenLastCalledWith({
@@ -164,7 +169,7 @@ describe('RunnerRegistrationTokenReset', () => {
});
window.confirm.mockReturnValueOnce(true);
- findButton().vm.$emit('click');
+ findDropdownItem().trigger('click');
await waitForPromises();
expect(createFlash).toHaveBeenLastCalledWith({
@@ -180,10 +185,10 @@ describe('RunnerRegistrationTokenReset', () => {
describe('Immediately after click', () => {
it('shows loading state', async () => {
window.confirm.mockReturnValue(true);
- findButton().vm.$emit('click');
+ findDropdownItem().trigger('click');
await nextTick();
- expect(findButton().props('loading')).toBe(true);
+ expect(findLoadingIcon().exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/runner/components/registration/registration_token_spec.js b/spec/frontend/runner/components/registration/registration_token_spec.js
new file mode 100644
index 00000000000..f53ae165344
--- /dev/null
+++ b/spec/frontend/runner/components/registration/registration_token_spec.js
@@ -0,0 +1,109 @@
+import { nextTick } from 'vue';
+import { GlToast } from '@gitlab/ui';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import RegistrationToken from '~/runner/components/registration/registration_token.vue';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+
+const mockToken = '01234567890';
+const mockMasked = '***********';
+
+describe('RegistrationToken', () => {
+ let wrapper;
+ let stopPropagation;
+ let showToast;
+
+ const findToggleMaskButton = () => wrapper.findByTestId('toggle-masked');
+ const findCopyButton = () => wrapper.findComponent(ModalCopyButton);
+
+ const vueWithGlToast = () => {
+ const localVue = createLocalVue();
+ localVue.use(GlToast);
+ return localVue;
+ };
+
+ const createComponent = ({ props = {}, withGlToast = true } = {}) => {
+ const localVue = withGlToast ? vueWithGlToast() : undefined;
+
+ wrapper = extendedWrapper(
+ shallowMount(RegistrationToken, {
+ propsData: {
+ value: mockToken,
+ ...props,
+ },
+ localVue,
+ }),
+ );
+
+ showToast = wrapper.vm.$toast ? jest.spyOn(wrapper.vm.$toast, 'show') : null;
+ };
+
+ beforeEach(() => {
+ stopPropagation = jest.fn();
+
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('Displays masked value by default', () => {
+ expect(wrapper.text()).toBe(mockMasked);
+ });
+
+ it('Displays button to reveal token', () => {
+ expect(findToggleMaskButton().attributes('aria-label')).toBe('Click to reveal');
+ });
+
+ it('Can copy the original token value', () => {
+ expect(findCopyButton().props('text')).toBe(mockToken);
+ });
+
+ describe('When the reveal icon is clicked', () => {
+ beforeEach(() => {
+ findToggleMaskButton().vm.$emit('click', { stopPropagation });
+ });
+
+ it('Click event is not propagated', async () => {
+ expect(stopPropagation).toHaveBeenCalledTimes(1);
+ });
+
+ it('Displays the actual value', () => {
+ expect(wrapper.text()).toBe(mockToken);
+ });
+
+ it('Can copy the original token value', () => {
+ expect(findCopyButton().props('text')).toBe(mockToken);
+ });
+
+ it('Displays button to mask token', () => {
+ expect(findToggleMaskButton().attributes('aria-label')).toBe('Click to hide');
+ });
+
+ it('When user clicks again, displays masked value', async () => {
+ findToggleMaskButton().vm.$emit('click', { stopPropagation });
+ await nextTick();
+
+ expect(wrapper.text()).toBe(mockMasked);
+ expect(findToggleMaskButton().attributes('aria-label')).toBe('Click to reveal');
+ });
+ });
+
+ describe('When the copy to clipboard button is clicked', () => {
+ it('shows a copied message', () => {
+ findCopyButton().vm.$emit('success');
+
+ expect(showToast).toHaveBeenCalledTimes(1);
+ expect(showToast).toHaveBeenCalledWith('Registration token copied!');
+ });
+
+ it('does not fail when toast is not defined', () => {
+ createComponent({ withGlToast: false });
+ findCopyButton().vm.$emit('success');
+
+ // This block also tests for unhandled errors
+ expect(showToast).toBeNull();
+ });
+ });
+});
diff --git a/spec/frontend/runner/components/runner_contacted_state_badge_spec.js b/spec/frontend/runner/components/runner_contacted_state_badge_spec.js
new file mode 100644
index 00000000000..57a27f39826
--- /dev/null
+++ b/spec/frontend/runner/components/runner_contacted_state_badge_spec.js
@@ -0,0 +1,86 @@
+import { GlBadge } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import RunnerContactedStateBadge from '~/runner/components/runner_contacted_state_badge.vue';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_NOT_CONNECTED } from '~/runner/constants';
+
+describe('RunnerTypeBadge', () => {
+ let wrapper;
+
+ const findBadge = () => wrapper.findComponent(GlBadge);
+ const getTooltip = () => getBinding(findBadge().element, 'gl-tooltip');
+
+ const createComponent = ({ runner = {} } = {}) => {
+ wrapper = shallowMount(RunnerContactedStateBadge, {
+ propsData: {
+ runner: {
+ contactedAt: '2021-01-01T00:00:00Z',
+ status: STATUS_ONLINE,
+ ...runner,
+ },
+ },
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ });
+ };
+
+ beforeEach(() => {
+ jest.useFakeTimers('modern');
+ });
+
+ afterEach(() => {
+ jest.useFakeTimers('legacy');
+
+ wrapper.destroy();
+ });
+
+ it('renders online state', () => {
+ jest.setSystemTime(new Date('2021-01-01T00:01:00Z'));
+
+ createComponent();
+
+ expect(wrapper.text()).toBe('online');
+ expect(findBadge().props('variant')).toBe('success');
+ expect(getTooltip().value).toBe('Runner is online; last contact was 1 minute ago');
+ });
+
+ it('renders offline state', () => {
+ jest.setSystemTime(new Date('2021-01-02T00:00:00Z'));
+
+ createComponent({
+ runner: {
+ status: STATUS_OFFLINE,
+ },
+ });
+
+ expect(wrapper.text()).toBe('offline');
+ expect(findBadge().props('variant')).toBe('muted');
+ expect(getTooltip().value).toBe(
+ 'No recent contact from this runner; last contact was 1 day ago',
+ );
+ });
+
+ it('renders not connected state', () => {
+ createComponent({
+ runner: {
+ contactedAt: null,
+ status: STATUS_NOT_CONNECTED,
+ },
+ });
+
+ expect(wrapper.text()).toBe('not connected');
+ expect(findBadge().props('variant')).toBe('muted');
+ expect(getTooltip().value).toMatch('This runner has never connected');
+ });
+
+ it('does not fail when data is missing', () => {
+ createComponent({
+ runner: {
+ status: null,
+ },
+ });
+
+ expect(wrapper.text()).toBe('');
+ });
+});
diff --git a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js b/spec/frontend/runner/components/runner_filtered_search_bar_spec.js
index 46948af1f28..9ea0955f2a1 100644
--- a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js
+++ b/spec/frontend/runner/components/runner_filtered_search_bar_spec.js
@@ -5,13 +5,7 @@ import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_
import { statusTokenConfig } from '~/runner/components/search_tokens/status_token_config';
import TagToken from '~/runner/components/search_tokens/tag_token.vue';
import { tagTokenConfig } from '~/runner/components/search_tokens/tag_token_config';
-import { typeTokenConfig } from '~/runner/components/search_tokens/type_token_config';
-import {
- PARAM_KEY_STATUS,
- PARAM_KEY_RUNNER_TYPE,
- PARAM_KEY_TAG,
- STATUS_ACTIVE,
-} from '~/runner/constants';
+import { PARAM_KEY_STATUS, PARAM_KEY_TAG, STATUS_ACTIVE, INSTANCE_TYPE } from '~/runner/constants';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
@@ -31,6 +25,11 @@ describe('RunnerList', () => {
];
const mockActiveRunnersCount = 2;
+ const expectToHaveLastEmittedInput = (value) => {
+ const inputs = wrapper.emitted('input');
+ expect(inputs[inputs.length - 1][0]).toEqual(value);
+ };
+
const createComponent = ({ props = {}, options = {} } = {}) => {
wrapper = extendedWrapper(
shallowMount(RunnerFilteredSearchBar, {
@@ -38,6 +37,7 @@ describe('RunnerList', () => {
namespace: 'runners',
tokens: [],
value: {
+ runnerType: null,
filters: [],
sort: mockDefaultSort,
},
@@ -86,7 +86,7 @@ describe('RunnerList', () => {
it('sets tokens to the filtered search', () => {
createComponent({
props: {
- tokens: [statusTokenConfig, typeTokenConfig, tagTokenConfig],
+ tokens: [statusTokenConfig, tagTokenConfig],
},
});
@@ -97,11 +97,6 @@ describe('RunnerList', () => {
options: expect.any(Array),
}),
expect.objectContaining({
- type: PARAM_KEY_RUNNER_TYPE,
- token: BaseToken,
- options: expect.any(Array),
- }),
- expect.objectContaining({
type: PARAM_KEY_TAG,
token: TagToken,
}),
@@ -123,6 +118,7 @@ describe('RunnerList', () => {
createComponent({
props: {
value: {
+ runnerType: INSTANCE_TYPE,
sort: mockOtherSort,
filters: mockFilters,
},
@@ -142,30 +138,40 @@ describe('RunnerList', () => {
.text(),
).toEqual('Last contact');
});
+
+ it('when the user sets a filter, the "search" preserves the other filters', () => {
+ findGlFilteredSearch().vm.$emit('input', mockFilters);
+ findGlFilteredSearch().vm.$emit('submit');
+
+ expectToHaveLastEmittedInput({
+ runnerType: INSTANCE_TYPE,
+ filters: mockFilters,
+ sort: mockOtherSort,
+ pagination: { page: 1 },
+ });
+ });
});
it('when the user sets a filter, the "search" is emitted with filters', () => {
findGlFilteredSearch().vm.$emit('input', mockFilters);
findGlFilteredSearch().vm.$emit('submit');
- expect(wrapper.emitted('input')[0]).toEqual([
- {
- filters: mockFilters,
- sort: mockDefaultSort,
- pagination: { page: 1 },
- },
- ]);
+ expectToHaveLastEmittedInput({
+ runnerType: null,
+ filters: mockFilters,
+ sort: mockDefaultSort,
+ pagination: { page: 1 },
+ });
});
it('when the user sets a sorting method, the "search" is emitted with the sort', () => {
findSortOptions().at(1).vm.$emit('click');
- expect(wrapper.emitted('input')[0]).toEqual([
- {
- filters: [],
- sort: mockOtherSort,
- pagination: { page: 1 },
- },
- ]);
+ expectToHaveLastEmittedInput({
+ runnerType: null,
+ filters: [],
+ sort: mockOtherSort,
+ pagination: { page: 1 },
+ });
});
});
diff --git a/spec/frontend/runner/components/runner_list_spec.js b/spec/frontend/runner/components/runner_list_spec.js
index e24dffea1eb..986e55a2132 100644
--- a/spec/frontend/runner/components/runner_list_spec.js
+++ b/spec/frontend/runner/components/runner_list_spec.js
@@ -1,6 +1,5 @@
import { GlTable, GlSkeletonLoader } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
-import { cloneDeep } from 'lodash';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerList from '~/runner/components/runner_list.vue';
@@ -43,12 +42,10 @@ describe('RunnerList', () => {
const headerLabels = findHeaders().wrappers.map((w) => w.text());
expect(headerLabels).toEqual([
- 'Type/State',
- 'Runner',
+ 'Status',
+ 'Runner ID',
'Version',
'IP Address',
- 'Projects',
- 'Jobs',
'Tags',
'Last contact',
'', // actions has no label
@@ -65,7 +62,7 @@ describe('RunnerList', () => {
const { id, description, version, ipAddress, shortSha } = mockRunners[0];
// Badges
- expect(findCell({ fieldKey: 'type' }).text()).toMatchInterpolatedText('specific paused');
+ expect(findCell({ fieldKey: 'status' }).text()).toMatchInterpolatedText('not connected paused');
// Runner summary
expect(findCell({ fieldKey: 'summary' }).text()).toContain(
@@ -76,8 +73,6 @@ describe('RunnerList', () => {
// Other fields
expect(findCell({ fieldKey: 'version' }).text()).toBe(version);
expect(findCell({ fieldKey: 'ipAddress' }).text()).toBe(ipAddress);
- expect(findCell({ fieldKey: 'projectCount' }).text()).toBe('1');
- expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('0');
expect(findCell({ fieldKey: 'tagList' }).text()).toBe('');
expect(findCell({ fieldKey: 'contactedAt' }).text()).toEqual(expect.any(String));
@@ -88,54 +83,6 @@ describe('RunnerList', () => {
expect(actions.findByTestId('toggle-active-runner').exists()).toBe(true);
});
- describe('Table data formatting', () => {
- let mockRunnersCopy;
-
- beforeEach(() => {
- mockRunnersCopy = cloneDeep(mockRunners);
- });
-
- it('Formats null project counts', () => {
- mockRunnersCopy[0].projectCount = null;
-
- createComponent({ props: { runners: mockRunnersCopy } }, mount);
-
- expect(findCell({ fieldKey: 'projectCount' }).text()).toBe('n/a');
- });
-
- it('Formats 0 project counts', () => {
- mockRunnersCopy[0].projectCount = 0;
-
- createComponent({ props: { runners: mockRunnersCopy } }, mount);
-
- expect(findCell({ fieldKey: 'projectCount' }).text()).toBe('0');
- });
-
- it('Formats big project counts', () => {
- mockRunnersCopy[0].projectCount = 1000;
-
- createComponent({ props: { runners: mockRunnersCopy } }, mount);
-
- expect(findCell({ fieldKey: 'projectCount' }).text()).toBe('1,000');
- });
-
- it('Formats job counts', () => {
- mockRunnersCopy[0].jobCount = 1000;
-
- createComponent({ props: { runners: mockRunnersCopy } }, mount);
-
- expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('1,000');
- });
-
- it('Formats big job counts with a plus symbol', () => {
- mockRunnersCopy[0].jobCount = 1001;
-
- createComponent({ props: { runners: mockRunnersCopy } }, mount);
-
- expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('1,000+');
- });
- });
-
it('Shows runner identifier', () => {
const { id, shortSha } = mockRunners[0];
const numericId = getIdFromGraphQLId(id);
diff --git a/spec/frontend/runner/components/runner_manual_setup_help_spec.js b/spec/frontend/runner/components/runner_manual_setup_help_spec.js
deleted file mode 100644
index effef0e7ebf..00000000000
--- a/spec/frontend/runner/components/runner_manual_setup_help_spec.js
+++ /dev/null
@@ -1,122 +0,0 @@
-import { GlSprintf } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import { TEST_HOST } from 'helpers/test_constants';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import MaskedValue from '~/runner/components/helpers/masked_value.vue';
-import RunnerManualSetupHelp from '~/runner/components/runner_manual_setup_help.vue';
-import RunnerRegistrationTokenReset from '~/runner/components/runner_registration_token_reset.vue';
-import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import RunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue';
-
-const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
-const mockRunnerInstallHelpPage = 'https://docs.gitlab.com/runner/install/';
-
-describe('RunnerManualSetupHelp', () => {
- let wrapper;
- let originalGon;
-
- const findRunnerInstructions = () => wrapper.findComponent(RunnerInstructions);
- const findRunnerRegistrationTokenReset = () =>
- wrapper.findComponent(RunnerRegistrationTokenReset);
- const findClipboardButtons = () => wrapper.findAllComponents(ClipboardButton);
- const findRunnerHelpTitle = () => wrapper.findByTestId('runner-help-title');
- const findCoordinatorUrl = () => wrapper.findByTestId('coordinator-url');
- const findRegistrationToken = () => wrapper.findByTestId('registration-token');
- const findRunnerHelpLink = () => wrapper.findByTestId('runner-help-link');
-
- const createComponent = ({ props = {} } = {}) => {
- wrapper = extendedWrapper(
- shallowMount(RunnerManualSetupHelp, {
- provide: {
- runnerInstallHelpPage: mockRunnerInstallHelpPage,
- },
- propsData: {
- registrationToken: mockRegistrationToken,
- type: INSTANCE_TYPE,
- ...props,
- },
- stubs: {
- MaskedValue,
- GlSprintf,
- },
- }),
- );
- };
-
- beforeAll(() => {
- originalGon = global.gon;
- global.gon = { gitlab_url: TEST_HOST };
- });
-
- afterAll(() => {
- global.gon = originalGon;
- });
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('Title contains the shared runner type', () => {
- createComponent({ props: { type: INSTANCE_TYPE } });
-
- expect(findRunnerHelpTitle().text()).toMatchInterpolatedText('Set up a shared runner manually');
- });
-
- it('Title contains the group runner type', () => {
- createComponent({ props: { type: GROUP_TYPE } });
-
- expect(findRunnerHelpTitle().text()).toMatchInterpolatedText('Set up a group runner manually');
- });
-
- it('Title contains the specific runner type', () => {
- createComponent({ props: { type: PROJECT_TYPE } });
-
- expect(findRunnerHelpTitle().text()).toMatchInterpolatedText(
- 'Set up a specific runner manually',
- );
- });
-
- it('Runner Install Page link', () => {
- expect(findRunnerHelpLink().attributes('href')).toBe(mockRunnerInstallHelpPage);
- });
-
- it('Displays the coordinator URL token', () => {
- expect(findCoordinatorUrl().text()).toBe(TEST_HOST);
- expect(findClipboardButtons().at(0).props('text')).toBe(TEST_HOST);
- });
-
- it('Displays the runner instructions', () => {
- expect(findRunnerInstructions().exists()).toBe(true);
- });
-
- it('Displays the registration token', async () => {
- findRegistrationToken().find('[data-testid="toggle-masked"]').vm.$emit('click');
-
- await nextTick();
-
- expect(findRegistrationToken().text()).toBe(mockRegistrationToken);
- expect(findClipboardButtons().at(1).props('text')).toBe(mockRegistrationToken);
- });
-
- it('Displays the runner registration token reset button', () => {
- expect(findRunnerRegistrationTokenReset().exists()).toBe(true);
- });
-
- it('Replaces the runner reset button', async () => {
- const mockNewRegistrationToken = 'NEW_MOCK_REGISTRATION_TOKEN';
-
- findRegistrationToken().find('[data-testid="toggle-masked"]').vm.$emit('click');
- findRunnerRegistrationTokenReset().vm.$emit('tokenReset', mockNewRegistrationToken);
-
- await nextTick();
-
- expect(findRegistrationToken().text()).toBe(mockNewRegistrationToken);
- expect(findClipboardButtons().at(1).props('text')).toBe(mockNewRegistrationToken);
- });
-});
diff --git a/spec/frontend/runner/components/runner_state_paused_badge_spec.js b/spec/frontend/runner/components/runner_paused_badge_spec.js
index 8df56d6e3f3..18cfcfae864 100644
--- a/spec/frontend/runner/components/runner_state_paused_badge_spec.js
+++ b/spec/frontend/runner/components/runner_paused_badge_spec.js
@@ -1,6 +1,6 @@
import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import RunnerStatePausedBadge from '~/runner/components/runner_state_paused_badge.vue';
+import RunnerStatePausedBadge from '~/runner/components/runner_paused_badge.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
describe('RunnerTypeBadge', () => {
diff --git a/spec/frontend/runner/components/runner_state_locked_badge_spec.js b/spec/frontend/runner/components/runner_state_locked_badge_spec.js
deleted file mode 100644
index e92b671f5a1..00000000000
--- a/spec/frontend/runner/components/runner_state_locked_badge_spec.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import { GlBadge } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import RunnerStateLockedBadge from '~/runner/components/runner_state_locked_badge.vue';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-
-describe('RunnerTypeBadge', () => {
- let wrapper;
-
- const findBadge = () => wrapper.findComponent(GlBadge);
- const getTooltip = () => getBinding(findBadge().element, 'gl-tooltip');
-
- const createComponent = ({ props = {} } = {}) => {
- wrapper = shallowMount(RunnerStateLockedBadge, {
- propsData: {
- ...props,
- },
- directives: {
- GlTooltip: createMockDirective(),
- },
- });
- };
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders locked state', () => {
- expect(wrapper.text()).toBe('locked');
- expect(findBadge().props('variant')).toBe('warning');
- });
-
- it('renders tooltip', () => {
- expect(getTooltip().value).toBeDefined();
- });
-
- it('passes arbitrary attributes to the badge', () => {
- createComponent({ props: { size: 'sm' } });
-
- expect(findBadge().props('size')).toBe('sm');
- });
-});
diff --git a/spec/frontend/runner/components/runner_tag_spec.js b/spec/frontend/runner/components/runner_tag_spec.js
index dda318f8153..bd05d4b2cfe 100644
--- a/spec/frontend/runner/components/runner_tag_spec.js
+++ b/spec/frontend/runner/components/runner_tag_spec.js
@@ -1,18 +1,35 @@
import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import RunnerTag from '~/runner/components/runner_tag.vue';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+
+const mockTag = 'tag1';
describe('RunnerTag', () => {
let wrapper;
const findBadge = () => wrapper.findComponent(GlBadge);
+ const getTooltipValue = () => getBinding(findBadge().element, 'gl-tooltip').value;
+
+ const setDimensions = ({ scrollWidth, offsetWidth }) => {
+ jest.spyOn(findBadge().element, 'scrollWidth', 'get').mockReturnValue(scrollWidth);
+ jest.spyOn(findBadge().element, 'offsetWidth', 'get').mockReturnValue(offsetWidth);
+
+ // Mock trigger resize
+ getBinding(findBadge().element, 'gl-resize-observer').value();
+ };
const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMount(RunnerTag, {
propsData: {
- tag: 'tag1',
+ tag: mockTag,
...props,
},
+ directives: {
+ GlTooltip: createMockDirective(),
+ GlResizeObserver: createMockDirective(),
+ },
});
};
@@ -25,21 +42,36 @@ describe('RunnerTag', () => {
});
it('Displays tag text', () => {
- expect(wrapper.text()).toBe('tag1');
+ expect(wrapper.text()).toBe(mockTag);
});
it('Displays tags with correct style', () => {
expect(findBadge().props()).toMatchObject({
- size: 'md',
- variant: 'info',
+ size: 'sm',
+ variant: 'neutral',
});
});
- it('Displays tags with small size', () => {
+ it('Displays tags with md size', () => {
createComponent({
- props: { size: 'sm' },
+ props: { size: 'md' },
});
- expect(findBadge().props('size')).toBe('sm');
+ expect(findBadge().props('size')).toBe('md');
});
+
+ it.each`
+ case | scrollWidth | offsetWidth | expectedTooltip
+ ${'overflowing'} | ${110} | ${100} | ${mockTag}
+ ${'not overflowing'} | ${90} | ${100} | ${''}
+ ${'almost overflowing'} | ${100} | ${100} | ${''}
+ `(
+ 'Sets "$expectedTooltip" as tooltip when $case',
+ async ({ scrollWidth, offsetWidth, expectedTooltip }) => {
+ setDimensions({ scrollWidth, offsetWidth });
+ await nextTick();
+
+ expect(getTooltipValue()).toBe(expectedTooltip);
+ },
+ );
});
diff --git a/spec/frontend/runner/components/runner_tags_spec.js b/spec/frontend/runner/components/runner_tags_spec.js
index b6487ade0d6..da89a659432 100644
--- a/spec/frontend/runner/components/runner_tags_spec.js
+++ b/spec/frontend/runner/components/runner_tags_spec.js
@@ -33,16 +33,16 @@ describe('RunnerTags', () => {
});
it('Displays tags with correct style', () => {
- expect(findBadge().props('size')).toBe('md');
- expect(findBadge().props('variant')).toBe('info');
+ expect(findBadge().props('size')).toBe('sm');
+ expect(findBadge().props('variant')).toBe('neutral');
});
- it('Displays tags with small size', () => {
+ it('Displays tags with md size', () => {
createComponent({
- props: { size: 'sm' },
+ props: { size: 'md' },
});
- expect(findBadge().props('size')).toBe('sm');
+ expect(findBadge().props('size')).toBe('md');
});
it('Is empty when there are no tags', () => {
diff --git a/spec/frontend/runner/components/runner_type_alert_spec.js b/spec/frontend/runner/components/runner_type_alert_spec.js
index e54e499743b..4023c75c9a8 100644
--- a/spec/frontend/runner/components/runner_type_alert_spec.js
+++ b/spec/frontend/runner/components/runner_type_alert_spec.js
@@ -23,11 +23,11 @@ describe('RunnerTypeAlert', () => {
});
describe.each`
- type | exampleText | anchor | variant
- ${INSTANCE_TYPE} | ${'This runner is available to all groups and projects'} | ${'#shared-runners'} | ${'success'}
- ${GROUP_TYPE} | ${'This runner is available to all projects and subgroups in a group'} | ${'#group-runners'} | ${'success'}
- ${PROJECT_TYPE} | ${'This runner is associated with one or more projects'} | ${'#specific-runners'} | ${'info'}
- `('When it is an $type level runner', ({ type, exampleText, anchor, variant }) => {
+ type | exampleText | anchor
+ ${INSTANCE_TYPE} | ${'This runner is available to all groups and projects'} | ${'#shared-runners'}
+ ${GROUP_TYPE} | ${'This runner is available to all projects and subgroups in a group'} | ${'#group-runners'}
+ ${PROJECT_TYPE} | ${'This runner is associated with one or more projects'} | ${'#specific-runners'}
+ `('When it is an $type level runner', ({ type, exampleText, anchor }) => {
beforeEach(() => {
createComponent({ props: { type } });
});
@@ -36,8 +36,8 @@ describe('RunnerTypeAlert', () => {
expect(wrapper.text()).toMatch(exampleText);
});
- it(`Shows a ${variant} variant`, () => {
- expect(findAlert().props('variant')).toBe(variant);
+ it(`Shows an "info" variant`, () => {
+ expect(findAlert().props('variant')).toBe('info');
});
it(`Links to anchor "${anchor}"`, () => {
diff --git a/spec/frontend/runner/components/runner_type_badge_spec.js b/spec/frontend/runner/components/runner_type_badge_spec.js
index fb344e65389..7bb0a2e6e2f 100644
--- a/spec/frontend/runner/components/runner_type_badge_spec.js
+++ b/spec/frontend/runner/components/runner_type_badge_spec.js
@@ -26,18 +26,18 @@ describe('RunnerTypeBadge', () => {
});
describe.each`
- type | text | variant
- ${INSTANCE_TYPE} | ${'shared'} | ${'success'}
- ${GROUP_TYPE} | ${'group'} | ${'success'}
- ${PROJECT_TYPE} | ${'specific'} | ${'info'}
- `('displays $type runner', ({ type, text, variant }) => {
+ type | text
+ ${INSTANCE_TYPE} | ${'shared'}
+ ${GROUP_TYPE} | ${'group'}
+ ${PROJECT_TYPE} | ${'specific'}
+ `('displays $type runner', ({ type, text }) => {
beforeEach(() => {
createComponent({ props: { type } });
});
- it(`as "${text}" with a ${variant} variant`, () => {
+ it(`as "${text}" with an "info" variant`, () => {
expect(findBadge().text()).toBe(text);
- expect(findBadge().props('variant')).toBe(variant);
+ expect(findBadge().props('variant')).toBe('info');
});
it('with a tooltip', () => {
diff --git a/spec/frontend/runner/components/runner_type_tabs_spec.js b/spec/frontend/runner/components/runner_type_tabs_spec.js
new file mode 100644
index 00000000000..4871d9c470a
--- /dev/null
+++ b/spec/frontend/runner/components/runner_type_tabs_spec.js
@@ -0,0 +1,109 @@
+import { GlTab } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
+import { INSTANCE_TYPE, GROUP_TYPE } from '~/runner/constants';
+
+const mockSearch = { runnerType: null, filters: [], pagination: { page: 1 }, sort: 'CREATED_DESC' };
+
+describe('RunnerTypeTabs', () => {
+ let wrapper;
+
+ const findTabs = () => wrapper.findAll(GlTab);
+ const findActiveTab = () =>
+ findTabs()
+ .filter((tab) => tab.attributes('active') === 'true')
+ .at(0);
+
+ const createComponent = ({ props, ...options } = {}) => {
+ wrapper = shallowMount(RunnerTypeTabs, {
+ propsData: {
+ value: mockSearch,
+ ...props,
+ },
+ stubs: {
+ GlTab,
+ },
+ ...options,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('Renders options to filter runners', () => {
+ expect(findTabs().wrappers.map((tab) => tab.text())).toEqual([
+ 'All',
+ 'Instance',
+ 'Group',
+ 'Project',
+ ]);
+ });
+
+ it('"All" is selected by default', () => {
+ expect(findActiveTab().text()).toBe('All');
+ });
+
+ it('Another tab can be preselected by the user', () => {
+ createComponent({
+ props: {
+ value: {
+ ...mockSearch,
+ runnerType: INSTANCE_TYPE,
+ },
+ },
+ });
+
+ expect(findActiveTab().text()).toBe('Instance');
+ });
+
+ describe('When the user selects a tab', () => {
+ const emittedValue = () => wrapper.emitted('input')[0][0];
+
+ beforeEach(() => {
+ findTabs().at(2).vm.$emit('click');
+ });
+
+ it(`Runner type is emitted`, () => {
+ expect(emittedValue()).toEqual({
+ ...mockSearch,
+ runnerType: GROUP_TYPE,
+ });
+ });
+
+ it('Runner type is selected', async () => {
+ const newValue = emittedValue();
+ await wrapper.setProps({ value: newValue });
+
+ expect(findActiveTab().text()).toBe('Group');
+ });
+ });
+
+ describe('When using a custom slot', () => {
+ const mockContent = 'content';
+
+ beforeEach(() => {
+ createComponent({
+ scopedSlots: {
+ title: `
+ <span>
+ {{props.tab.title}} ${mockContent}
+ </span>`,
+ },
+ });
+ });
+
+ it('Renders tabs with additional information', () => {
+ expect(findTabs().wrappers.map((tab) => tab.text())).toEqual([
+ `All ${mockContent}`,
+ `Instance ${mockContent}`,
+ `Group ${mockContent}`,
+ `Project ${mockContent}`,
+ ]);
+ });
+ });
+});
diff --git a/spec/frontend/runner/group_runners/group_runners_app_spec.js b/spec/frontend/runner/group_runners/group_runners_app_spec.js
index 5f3aabd4bc3..39bca743c80 100644
--- a/spec/frontend/runner/group_runners/group_runners_app_spec.js
+++ b/spec/frontend/runner/group_runners/group_runners_app_spec.js
@@ -1,3 +1,4 @@
+import { nextTick } from 'vue';
import { GlLink } from '@gitlab/ui';
import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
@@ -11,7 +12,7 @@ import { updateHistory } from '~/lib/utils/url_utility';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
import RunnerList from '~/runner/components/runner_list.vue';
-import RunnerManualSetupHelp from '~/runner/components/runner_manual_setup_help.vue';
+import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
import RunnerPagination from '~/runner/components/runner_pagination.vue';
import {
@@ -19,8 +20,8 @@ import {
CREATED_DESC,
DEFAULT_SORT,
INSTANCE_TYPE,
+ GROUP_TYPE,
PARAM_KEY_STATUS,
- PARAM_KEY_RUNNER_TYPE,
STATUS_ACTIVE,
RUNNER_PAGE_SIZE,
} from '~/runner/constants';
@@ -48,7 +49,7 @@ describe('GroupRunnersApp', () => {
let wrapper;
let mockGroupRunnersQuery;
- const findRunnerManualSetupHelp = () => wrapper.findComponent(RunnerManualSetupHelp);
+ const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
const findRunnerList = () => wrapper.findComponent(RunnerList);
const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
const findRunnerPaginationPrev = () =>
@@ -82,13 +83,13 @@ describe('GroupRunnersApp', () => {
});
it('shows the runner setup instructions', () => {
- expect(findRunnerManualSetupHelp().props('registrationToken')).toBe(mockRegistrationToken);
+ expect(findRegistrationDropdown().props('registrationToken')).toBe(mockRegistrationToken);
+ expect(findRegistrationDropdown().props('type')).toBe(GROUP_TYPE);
});
it('shows the runners list', () => {
- expect(findRunnerList().props('runners')).toEqual(
- groupRunnersData.data.group.runners.edges.map(({ node }) => node),
- );
+ const runners = findRunnerList().props('runners');
+ expect(runners).toEqual(groupRunnersData.data.group.runners.edges.map(({ node }) => node));
});
it('runner item links to the runner group page', async () => {
@@ -117,16 +118,15 @@ describe('GroupRunnersApp', () => {
it('sets tokens in the filtered search', () => {
createComponent({ mountFn: mount });
- expect(findFilteredSearch().props('tokens')).toEqual([
+ const tokens = findFilteredSearch().props('tokens');
+
+ expect(tokens).toHaveLength(1);
+ expect(tokens[0]).toEqual(
expect.objectContaining({
type: PARAM_KEY_STATUS,
options: expect.any(Array),
}),
- expect.objectContaining({
- type: PARAM_KEY_RUNNER_TYPE,
- options: expect.any(Array),
- }),
- ]);
+ );
});
describe('shows the active runner count', () => {
@@ -161,10 +161,8 @@ describe('GroupRunnersApp', () => {
it('sets the filters in the search bar', () => {
expect(findRunnerFilteredSearchBar().props('value')).toEqual({
- filters: [
- { type: 'status', value: { data: STATUS_ACTIVE, operator: '=' } },
- { type: 'runner_type', value: { data: INSTANCE_TYPE, operator: '=' } },
- ],
+ runnerType: INSTANCE_TYPE,
+ filters: [{ type: 'status', value: { data: STATUS_ACTIVE, operator: '=' } }],
sort: 'CREATED_DESC',
pagination: { page: 1 },
});
@@ -182,11 +180,14 @@ describe('GroupRunnersApp', () => {
});
describe('when a filter is selected by the user', () => {
- beforeEach(() => {
+ beforeEach(async () => {
findRunnerFilteredSearchBar().vm.$emit('input', {
+ runnerType: null,
filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ACTIVE, operator: '=' } }],
sort: CREATED_ASC,
});
+
+ await nextTick();
});
it('updates the browser url', () => {
diff --git a/spec/frontend/runner/runner_search_utils_spec.js b/spec/frontend/runner/runner_search_utils_spec.js
index 3a0c3abe7bd..0fc7917663e 100644
--- a/spec/frontend/runner/runner_search_utils_spec.js
+++ b/spec/frontend/runner/runner_search_utils_spec.js
@@ -1,5 +1,6 @@
import { RUNNER_PAGE_SIZE } from '~/runner/constants';
import {
+ searchValidator,
fromUrlQueryToSearch,
fromSearchToUrl,
fromSearchToVariables,
@@ -10,13 +11,14 @@ describe('search_params.js', () => {
{
name: 'a default query',
urlQuery: '',
- search: { filters: [], pagination: { page: 1 }, sort: 'CREATED_DESC' },
+ search: { runnerType: null, filters: [], pagination: { page: 1 }, sort: 'CREATED_DESC' },
graphqlVariables: { sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
},
{
name: 'a single status',
urlQuery: '?status[]=ACTIVE',
search: {
+ runnerType: null,
filters: [{ type: 'status', value: { data: 'ACTIVE', operator: '=' } }],
pagination: { page: 1 },
sort: 'CREATED_DESC',
@@ -27,6 +29,7 @@ describe('search_params.js', () => {
name: 'a single term text search',
urlQuery: '?search=something',
search: {
+ runnerType: null,
filters: [
{
type: 'filtered-search-term',
@@ -42,6 +45,7 @@ describe('search_params.js', () => {
name: 'a two terms text search',
urlQuery: '?search=something+else',
search: {
+ runnerType: null,
filters: [
{
type: 'filtered-search-term',
@@ -61,7 +65,8 @@ describe('search_params.js', () => {
name: 'single instance type',
urlQuery: '?runner_type[]=INSTANCE_TYPE',
search: {
- filters: [{ type: 'runner_type', value: { data: 'INSTANCE_TYPE', operator: '=' } }],
+ runnerType: 'INSTANCE_TYPE',
+ filters: [],
pagination: { page: 1 },
sort: 'CREATED_DESC',
},
@@ -71,6 +76,7 @@ describe('search_params.js', () => {
name: 'multiple runner status',
urlQuery: '?status[]=ACTIVE&status[]=PAUSED',
search: {
+ runnerType: null,
filters: [
{ type: 'status', value: { data: 'ACTIVE', operator: '=' } },
{ type: 'status', value: { data: 'PAUSED', operator: '=' } },
@@ -84,10 +90,8 @@ describe('search_params.js', () => {
name: 'multiple status, a single instance type and a non default sort',
urlQuery: '?status[]=ACTIVE&runner_type[]=INSTANCE_TYPE&sort=CREATED_ASC',
search: {
- filters: [
- { type: 'status', value: { data: 'ACTIVE', operator: '=' } },
- { type: 'runner_type', value: { data: 'INSTANCE_TYPE', operator: '=' } },
- ],
+ runnerType: 'INSTANCE_TYPE',
+ filters: [{ type: 'status', value: { data: 'ACTIVE', operator: '=' } }],
pagination: { page: 1 },
sort: 'CREATED_ASC',
},
@@ -102,6 +106,7 @@ describe('search_params.js', () => {
name: 'a tag',
urlQuery: '?tag[]=tag-1',
search: {
+ runnerType: null,
filters: [{ type: 'tag', value: { data: 'tag-1', operator: '=' } }],
pagination: { page: 1 },
sort: 'CREATED_DESC',
@@ -116,6 +121,7 @@ describe('search_params.js', () => {
name: 'two tags',
urlQuery: '?tag[]=tag-1&tag[]=tag-2',
search: {
+ runnerType: null,
filters: [
{ type: 'tag', value: { data: 'tag-1', operator: '=' } },
{ type: 'tag', value: { data: 'tag-2', operator: '=' } },
@@ -132,13 +138,19 @@ describe('search_params.js', () => {
{
name: 'the next page',
urlQuery: '?page=2&after=AFTER_CURSOR',
- search: { filters: [], pagination: { page: 2, after: 'AFTER_CURSOR' }, sort: 'CREATED_DESC' },
+ search: {
+ runnerType: null,
+ filters: [],
+ pagination: { page: 2, after: 'AFTER_CURSOR' },
+ sort: 'CREATED_DESC',
+ },
graphqlVariables: { sort: 'CREATED_DESC', after: 'AFTER_CURSOR', first: RUNNER_PAGE_SIZE },
},
{
name: 'the previous page',
urlQuery: '?page=2&before=BEFORE_CURSOR',
search: {
+ runnerType: null,
filters: [],
pagination: { page: 2, before: 'BEFORE_CURSOR' },
sort: 'CREATED_DESC',
@@ -150,9 +162,9 @@ describe('search_params.js', () => {
urlQuery:
'?status[]=ACTIVE&runner_type[]=INSTANCE_TYPE&tag[]=tag-1&tag[]=tag-2&sort=CREATED_ASC&page=2&after=AFTER_CURSOR',
search: {
+ runnerType: 'INSTANCE_TYPE',
filters: [
{ type: 'status', value: { data: 'ACTIVE', operator: '=' } },
- { type: 'runner_type', value: { data: 'INSTANCE_TYPE', operator: '=' } },
{ type: 'tag', value: { data: 'tag-1', operator: '=' } },
{ type: 'tag', value: { data: 'tag-2', operator: '=' } },
],
@@ -170,6 +182,14 @@ describe('search_params.js', () => {
},
];
+ describe('searchValidator', () => {
+ examples.forEach(({ name, search }) => {
+ it(`Validates ${name} as a search object`, () => {
+ expect(searchValidator(search)).toBe(true);
+ });
+ });
+ });
+
describe('fromUrlQueryToSearch', () => {
examples.forEach(({ name, urlQuery, search }) => {
it(`Converts ${name} to a search object`, () => {
diff --git a/spec/frontend/search/sidebar/components/app_spec.js b/spec/frontend/search/sidebar/components/app_spec.js
index b93527c1fe9..3bea0748c47 100644
--- a/spec/frontend/search/sidebar/components/app_spec.js
+++ b/spec/frontend/search/sidebar/components/app_spec.js
@@ -1,13 +1,13 @@
import { GlButton, GlLink } from '@gitlab/ui';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import { MOCK_QUERY } from 'jest/search/mock_data';
import GlobalSearchSidebar from '~/search/sidebar/components/app.vue';
import ConfidentialityFilter from '~/search/sidebar/components/confidentiality_filter.vue';
import StatusFilter from '~/search/sidebar/components/status_filter.vue';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('GlobalSearchSidebar', () => {
let wrapper;
@@ -20,28 +20,26 @@ describe('GlobalSearchSidebar', () => {
const createComponent = (initialState) => {
const store = new Vuex.Store({
state: {
- query: MOCK_QUERY,
+ urlQuery: MOCK_QUERY,
...initialState,
},
actions: actionSpies,
});
wrapper = shallowMount(GlobalSearchSidebar, {
- localVue,
store,
});
};
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
const findSidebarForm = () => wrapper.find('form');
- const findStatusFilter = () => wrapper.find(StatusFilter);
- const findConfidentialityFilter = () => wrapper.find(ConfidentialityFilter);
- const findApplyButton = () => wrapper.find(GlButton);
- const findResetLinkButton = () => wrapper.find(GlLink);
+ const findStatusFilter = () => wrapper.findComponent(StatusFilter);
+ const findConfidentialityFilter = () => wrapper.findComponent(ConfidentialityFilter);
+ const findApplyButton = () => wrapper.findComponent(GlButton);
+ const findResetLinkButton = () => wrapper.findComponent(GlLink);
describe('template', () => {
beforeEach(() => {
@@ -61,10 +59,32 @@ describe('GlobalSearchSidebar', () => {
});
});
+ describe('ApplyButton', () => {
+ describe('when sidebarDirty is false', () => {
+ beforeEach(() => {
+ createComponent({ sidebarDirty: false });
+ });
+
+ it('disables the button', () => {
+ expect(findApplyButton().attributes('disabled')).toBe('true');
+ });
+ });
+
+ describe('when sidebarDirty is true', () => {
+ beforeEach(() => {
+ createComponent({ sidebarDirty: true });
+ });
+
+ it('enables the button', () => {
+ expect(findApplyButton().attributes('disabled')).toBe(undefined);
+ });
+ });
+ });
+
describe('ResetLinkButton', () => {
describe('with no filter selected', () => {
beforeEach(() => {
- createComponent({ query: {} });
+ createComponent({ urlQuery: {} });
});
it('does not render', () => {
@@ -74,10 +94,20 @@ describe('GlobalSearchSidebar', () => {
describe('with filter selected', () => {
beforeEach(() => {
- createComponent();
+ createComponent({ urlQuery: MOCK_QUERY });
+ });
+
+ it('does render', () => {
+ expect(findResetLinkButton().exists()).toBe(true);
+ });
+ });
+
+ describe('with filter selected and user updated query back to default', () => {
+ beforeEach(() => {
+ createComponent({ urlQuery: MOCK_QUERY, query: {} });
});
- it('does render when a filter selected', () => {
+ it('does render', () => {
expect(findResetLinkButton().exists()).toBe(true);
});
});
diff --git a/spec/frontend/search/store/actions_spec.js b/spec/frontend/search/store/actions_spec.js
index b50248bb295..5f8cee8160f 100644
--- a/spec/frontend/search/store/actions_spec.js
+++ b/spec/frontend/search/store/actions_spec.js
@@ -5,7 +5,11 @@ 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 { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY } from '~/search/store/constants';
+import {
+ GROUPS_LOCAL_STORAGE_KEY,
+ PROJECTS_LOCAL_STORAGE_KEY,
+ SIDEBAR_PARAMS,
+} from '~/search/store/constants';
import * as types from '~/search/store/mutation_types';
import createState from '~/search/store/state';
import * as storeUtils from '~/search/store/utils';
@@ -153,15 +157,24 @@ describe('Global Search Store Actions', () => {
});
});
- describe('setQuery', () => {
- const payload = { key: 'key1', value: 'value1' };
+ describe.each`
+ payload | isDirty | isDirtyMutation
+ ${{ key: SIDEBAR_PARAMS[0], value: 'test' }} | ${false} | ${[{ type: types.SET_SIDEBAR_DIRTY, payload: false }]}
+ ${{ key: SIDEBAR_PARAMS[0], value: 'test' }} | ${true} | ${[{ type: types.SET_SIDEBAR_DIRTY, payload: true }]}
+ ${{ key: SIDEBAR_PARAMS[1], value: 'test' }} | ${false} | ${[{ type: types.SET_SIDEBAR_DIRTY, payload: false }]}
+ ${{ key: SIDEBAR_PARAMS[1], value: 'test' }} | ${true} | ${[{ type: types.SET_SIDEBAR_DIRTY, payload: true }]}
+ ${{ key: 'non-sidebar', value: 'test' }} | ${false} | ${[]}
+ ${{ key: 'non-sidebar', value: 'test' }} | ${true} | ${[]}
+ `('setQuery', ({ payload, isDirty, isDirtyMutation }) => {
+ describe(`when filter param is ${payload.key} and utils.isSidebarDirty returns ${isDirty}`, () => {
+ const expectedMutations = [{ type: types.SET_QUERY, payload }].concat(isDirtyMutation);
- it('calls the SET_QUERY mutation', () => {
- return testAction({
- action: actions.setQuery,
- payload,
- state,
- expectedMutations: [{ type: types.SET_QUERY, payload }],
+ beforeEach(() => {
+ storeUtils.isSidebarDirty = jest.fn().mockReturnValue(isDirty);
+ });
+
+ it(`should dispatch the correct mutations`, () => {
+ return testAction({ action: actions.setQuery, payload, state, expectedMutations });
});
});
});
diff --git a/spec/frontend/search/store/mutations_spec.js b/spec/frontend/search/store/mutations_spec.js
index a60718a972d..25f9b692955 100644
--- a/spec/frontend/search/store/mutations_spec.js
+++ b/spec/frontend/search/store/mutations_spec.js
@@ -72,6 +72,16 @@ describe('Global Search Store Mutations', () => {
});
});
+ describe('SET_SIDEBAR_DIRTY', () => {
+ const value = true;
+
+ it('sets sidebarDirty to the value', () => {
+ mutations[types.SET_SIDEBAR_DIRTY](state, value);
+
+ expect(state.sidebarDirty).toBe(value);
+ });
+ });
+
describe('LOAD_FREQUENT_ITEMS', () => {
it('sets frequentItems[key] to data', () => {
const payload = { key: 'test-key', data: [1, 2, 3] };
diff --git a/spec/frontend/search/store/utils_spec.js b/spec/frontend/search/store/utils_spec.js
index bcdad9f89dd..20d764190b1 100644
--- a/spec/frontend/search/store/utils_spec.js
+++ b/spec/frontend/search/store/utils_spec.js
@@ -1,6 +1,11 @@
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
-import { MAX_FREQUENCY } from '~/search/store/constants';
-import { loadDataFromLS, setFrequentItemToLS, mergeById } from '~/search/store/utils';
+import { MAX_FREQUENCY, SIDEBAR_PARAMS } from '~/search/store/constants';
+import {
+ loadDataFromLS,
+ setFrequentItemToLS,
+ mergeById,
+ isSidebarDirty,
+} from '~/search/store/utils';
import {
MOCK_LS_KEY,
MOCK_GROUPS,
@@ -216,4 +221,24 @@ describe('Global Search Store Utils', () => {
});
});
});
+
+ describe.each`
+ description | currentQuery | urlQuery | isDirty
+ ${'identical'} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'default' }} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'default' }} | ${false}
+ ${'different'} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'new' }} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'default' }} | ${true}
+ ${'null/undefined'} | ${{ [SIDEBAR_PARAMS[0]]: null, [SIDEBAR_PARAMS[1]]: null }} | ${{ [SIDEBAR_PARAMS[0]]: undefined, [SIDEBAR_PARAMS[1]]: undefined }} | ${false}
+ ${'updated/undefined'} | ${{ [SIDEBAR_PARAMS[0]]: 'new', [SIDEBAR_PARAMS[1]]: 'new' }} | ${{ [SIDEBAR_PARAMS[0]]: undefined, [SIDEBAR_PARAMS[1]]: undefined }} | ${true}
+ `('isSidebarDirty', ({ description, currentQuery, urlQuery, isDirty }) => {
+ describe(`with ${description} sidebar query data`, () => {
+ let res;
+
+ beforeEach(() => {
+ res = isSidebarDirty(currentQuery, urlQuery);
+ });
+
+ it(`returns ${isDirty}`, () => {
+ expect(res).toStrictEqual(isDirty);
+ });
+ });
+ });
});
diff --git a/spec/frontend/security_configuration/components/app_spec.js b/spec/frontend/security_configuration/components/app_spec.js
index f27f45f2b26..d4ee9e6e43d 100644
--- a/spec/frontend/security_configuration/components/app_spec.js
+++ b/spec/frontend/security_configuration/components/app_spec.js
@@ -1,5 +1,6 @@
import { GlTab } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
import stubChildren from 'helpers/stub_children';
@@ -70,6 +71,7 @@ describe('App component', () => {
const findTabs = () => wrapper.findAllComponents(GlTab);
const findByTestId = (id) => wrapper.findByTestId(id);
const findFeatureCards = () => wrapper.findAllComponents(FeatureCard);
+ const findManageViaMRErrorAlert = () => wrapper.findByTestId('manage-via-mr-error-alert');
const findLink = ({ href, text, container = wrapper }) => {
const selector = `a[href="${href}"]`;
const link = container.find(selector);
@@ -132,12 +134,12 @@ describe('App component', () => {
it('renders main-heading with correct text', () => {
const mainHeading = findMainHeading();
- expect(mainHeading).toExist();
+ expect(mainHeading.exists()).toBe(true);
expect(mainHeading.text()).toContain('Security Configuration');
});
it('renders GlTab Component ', () => {
- expect(findTab()).toExist();
+ expect(findTab().exists()).toBe(true);
});
it('renders right amount of tabs with correct title ', () => {
@@ -173,6 +175,43 @@ describe('App component', () => {
});
});
+ describe('Manage via MR Error Alert', () => {
+ beforeEach(() => {
+ createComponent({
+ augmentedSecurityFeatures: securityFeaturesMock,
+ augmentedComplianceFeatures: complianceFeaturesMock,
+ });
+ });
+
+ describe('on initial load', () => {
+ it('should not show Manage via MR Error Alert', () => {
+ expect(findManageViaMRErrorAlert().exists()).toBe(false);
+ });
+ });
+
+ describe('when error occurs', () => {
+ it('should show Alert with error Message', async () => {
+ expect(findManageViaMRErrorAlert().exists()).toBe(false);
+ findFeatureCards().at(1).vm.$emit('error', 'There was a manage via MR error');
+
+ await nextTick();
+ expect(findManageViaMRErrorAlert().exists()).toBe(true);
+ expect(findManageViaMRErrorAlert().text()).toEqual('There was a manage via MR error');
+ });
+
+ it('should hide Alert when it is dismissed', async () => {
+ findFeatureCards().at(1).vm.$emit('error', 'There was a manage via MR error');
+
+ await nextTick();
+ expect(findManageViaMRErrorAlert().exists()).toBe(true);
+
+ findManageViaMRErrorAlert().vm.$emit('dismiss');
+ await nextTick();
+ expect(findManageViaMRErrorAlert().exists()).toBe(false);
+ });
+ });
+ });
+
describe('Auto DevOps hint alert', () => {
describe('given the right props', () => {
beforeEach(() => {
diff --git a/spec/frontend/security_configuration/components/feature_card_spec.js b/spec/frontend/security_configuration/components/feature_card_spec.js
index fdb1d2f86e3..0eca2c27075 100644
--- a/spec/frontend/security_configuration/components/feature_card_spec.js
+++ b/spec/frontend/security_configuration/components/feature_card_spec.js
@@ -80,7 +80,11 @@ describe('FeatureCard component', () => {
describe('basic structure', () => {
beforeEach(() => {
- feature = makeFeature();
+ feature = makeFeature({
+ type: 'sast',
+ available: true,
+ canEnableByMergeRequest: true,
+ });
createComponent({ feature });
});
@@ -97,6 +101,11 @@ describe('FeatureCard component', () => {
expect(links.exists()).toBe(true);
expect(links).toHaveLength(1);
});
+
+ it('should catch and emit manage-via-mr-error', () => {
+ findManageViaMr().vm.$emit('error', 'There was a manage via MR error');
+ expect(wrapper.emitted('error')).toEqual([['There was a manage via MR error']]);
+ });
});
describe('status', () => {
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 7e81df1d7d2..c72c23a3a60 100644
--- a/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
+++ b/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
@@ -10,7 +10,7 @@ const DEFAULT_RENDER_COUNT = 5;
describe('UncollapsedAssigneeList component', () => {
let wrapper;
- function createComponent(props = {}) {
+ function createComponent(props = {}, glFeatures = {}) {
const propsData = {
users: [],
rootPath: TEST_HOST,
@@ -19,6 +19,7 @@ describe('UncollapsedAssigneeList component', () => {
wrapper = mount(UncollapsedAssigneeList, {
propsData,
+ provide: { glFeatures },
});
}
@@ -99,4 +100,22 @@ describe('UncollapsedAssigneeList component', () => {
});
});
});
+
+ describe('merge requests', () => {
+ it.each`
+ numberOfUsers
+ ${1}
+ ${5}
+ `('displays as a vertical list for $numberOfUsers of users', ({ numberOfUsers }) => {
+ createComponent(
+ {
+ users: UsersMockHelper.createNumberRandomUsers(numberOfUsers),
+ issuableType: 'merge_request',
+ },
+ { mrAttentionRequests: true },
+ );
+
+ expect(wrapper.findAll('[data-testid="username"]').length).toBe(numberOfUsers);
+ });
+ });
});
diff --git a/spec/frontend/sidebar/components/attention_required_toggle_spec.js b/spec/frontend/sidebar/components/attention_required_toggle_spec.js
new file mode 100644
index 00000000000..8555068cdd8
--- /dev/null
+++ b/spec/frontend/sidebar/components/attention_required_toggle_spec.js
@@ -0,0 +1,84 @@
+import { GlButton } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import AttentionRequestedToggle from '~/sidebar/components/attention_requested_toggle.vue';
+
+let wrapper;
+
+function factory(propsData = {}) {
+ wrapper = mount(AttentionRequestedToggle, { propsData });
+}
+
+const findToggle = () => wrapper.findComponent(GlButton);
+
+describe('Attention require toggle', () => {
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders button', () => {
+ factory({ type: 'reviewer', user: { attention_requested: false } });
+
+ expect(findToggle().exists()).toBe(true);
+ });
+
+ it.each`
+ attentionRequested | icon
+ ${true} | ${'star'}
+ ${false} | ${'star-o'}
+ `(
+ 'renders $icon icon when attention_requested is $attentionRequested',
+ ({ attentionRequested, icon }) => {
+ factory({ type: 'reviewer', user: { attention_requested: attentionRequested } });
+
+ expect(findToggle().props('icon')).toBe(icon);
+ },
+ );
+
+ it.each`
+ attentionRequested | variant
+ ${true} | ${'warning'}
+ ${false} | ${'default'}
+ `(
+ 'renders button with variant $variant when attention_requested is $attentionRequested',
+ ({ attentionRequested, variant }) => {
+ factory({ type: 'reviewer', user: { attention_requested: attentionRequested } });
+
+ expect(findToggle().props('variant')).toBe(variant);
+ },
+ );
+
+ it('emits toggle-attention-requested on click', async () => {
+ factory({ type: 'reviewer', user: { attention_requested: true } });
+
+ await findToggle().trigger('click');
+
+ expect(wrapper.emitted('toggle-attention-requested')[0]).toEqual([
+ {
+ user: { attention_requested: true },
+ callback: expect.anything(),
+ },
+ ]);
+ });
+
+ it('sets loading on click', async () => {
+ factory({ type: 'reviewer', user: { attention_requested: true } });
+
+ await findToggle().trigger('click');
+
+ expect(findToggle().props('loading')).toBe(true);
+ });
+
+ it.each`
+ type | attentionRequested | tooltip
+ ${'reviewer'} | ${true} | ${AttentionRequestedToggle.i18n.removeAttentionRequested}
+ ${'reviewer'} | ${false} | ${AttentionRequestedToggle.i18n.attentionRequestedReviewer}
+ ${'assignee'} | ${false} | ${AttentionRequestedToggle.i18n.attentionRequestedAssignee}
+ `(
+ 'sets tooltip as $tooltip when attention_requested is $attentionRequested and type is $type',
+ ({ type, attentionRequested, tooltip }) => {
+ factory({ type, user: { attention_requested: attentionRequested } });
+
+ expect(findToggle().attributes('aria-label')).toBe(tooltip);
+ },
+ );
+});
diff --git a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
index 6b80224083a..13887f28d22 100644
--- a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
+++ b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
@@ -1,5 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
+import AttentionRequestedToggle from '~/sidebar/components/attention_requested_toggle.vue';
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';
@@ -9,7 +10,7 @@ describe('UncollapsedReviewerList component', () => {
const reviewerApprovalIcons = () => wrapper.findAll('[data-testid="re-approved"]');
- function createComponent(props = {}) {
+ function createComponent(props = {}, glFeatures = {}) {
const propsData = {
users: [],
rootPath: TEST_HOST,
@@ -18,6 +19,9 @@ describe('UncollapsedReviewerList component', () => {
wrapper = shallowMount(UncollapsedReviewerList, {
propsData,
+ provide: {
+ glFeatures,
+ },
});
}
@@ -110,4 +114,18 @@ describe('UncollapsedReviewerList component', () => {
expect(wrapper.find('[data-testid="re-request-success"]').exists()).toBe(true);
});
});
+
+ it('hides re-request review button when attentionRequired feature flag is enabled', () => {
+ createComponent({ users: [userDataMock()] }, { mrAttentionRequests: true });
+
+ expect(wrapper.findAll('[data-testid="re-request-button"]').length).toBe(0);
+ });
+
+ it('emits toggle-attention-requested', () => {
+ createComponent({ users: [userDataMock()] }, { mrAttentionRequests: true });
+
+ wrapper.find(AttentionRequestedToggle).vm.$emit('toggle-attention-requested', 'data');
+
+ expect(wrapper.emitted('toggle-attention-requested')[0]).toEqual(['data']);
+ });
});
diff --git a/spec/frontend/sidebar/components/time_tracking/report_spec.js b/spec/frontend/sidebar/components/time_tracking/report_spec.js
index 66218626e6b..64d143615a0 100644
--- a/spec/frontend/sidebar/components/time_tracking/report_spec.js
+++ b/spec/frontend/sidebar/components/time_tracking/report_spec.js
@@ -50,7 +50,7 @@ describe('Issuable Time Tracking Report', () => {
it('should render loading spinner', () => {
mountComponent();
- expect(findLoadingIcon()).toExist();
+ expect(findLoadingIcon().exists()).toBe(true);
});
it('should render error message on reject', async () => {
diff --git a/spec/frontend/sidebar/sidebar_mediator_spec.js b/spec/frontend/sidebar/sidebar_mediator_spec.js
index cb84c142d55..3d7baaff10a 100644
--- a/spec/frontend/sidebar/sidebar_mediator_spec.js
+++ b/spec/frontend/sidebar/sidebar_mediator_spec.js
@@ -4,8 +4,11 @@ import * as urlUtility from '~/lib/utils/url_utility';
import SidebarService, { gqClient } from '~/sidebar/services/sidebar_service';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarStore from '~/sidebar/stores/sidebar_store';
+import toast from '~/vue_shared/plugins/global_toast';
import Mock from './mock_data';
+jest.mock('~/vue_shared/plugins/global_toast');
+
describe('Sidebar mediator', () => {
const { mediator: mediatorMockData } = Mock;
let mock;
@@ -115,4 +118,56 @@ describe('Sidebar mediator', () => {
urlSpy.mockRestore();
});
});
+
+ describe('toggleAttentionRequested', () => {
+ let attentionRequiredService;
+
+ beforeEach(() => {
+ attentionRequiredService = jest
+ .spyOn(mediator.service, 'toggleAttentionRequested')
+ .mockResolvedValue();
+ });
+
+ it('calls attentionRequired service method', async () => {
+ mediator.store.reviewers = [{ id: 1, attention_requested: false, username: 'root' }];
+
+ await mediator.toggleAttentionRequested('reviewer', {
+ user: { id: 1, username: 'root' },
+ callback: jest.fn(),
+ });
+
+ expect(attentionRequiredService).toHaveBeenCalledWith(1);
+ });
+
+ it.each`
+ type | method
+ ${'reviewer'} | ${'findReviewer'}
+ `('finds $type', ({ type, method }) => {
+ const methodSpy = jest.spyOn(mediator.store, method);
+
+ mediator.toggleAttentionRequested(type, { user: { id: 1 }, callback: jest.fn() });
+
+ expect(methodSpy).toHaveBeenCalledWith({ id: 1 });
+ });
+
+ it.each`
+ attentionRequested | toastMessage
+ ${true} | ${'Removed attention request from @root'}
+ ${false} | ${'Requested attention from @root'}
+ `(
+ 'it creates toast $toastMessage when attention_requested is $attentionRequested',
+ async ({ attentionRequested, toastMessage }) => {
+ mediator.store.reviewers = [
+ { id: 1, attention_requested: attentionRequested, username: 'root' },
+ ];
+
+ await mediator.toggleAttentionRequested('reviewer', {
+ user: { id: 1, username: 'root' },
+ callback: jest.fn(),
+ });
+
+ expect(toast).toHaveBeenCalledWith(toastMessage);
+ },
+ );
+ });
});
diff --git a/spec/frontend/task_list_spec.js b/spec/frontend/task_list_spec.js
index 2d7a735bd11..bf470e7e126 100644
--- a/spec/frontend/task_list_spec.js
+++ b/spec/frontend/task_list_spec.js
@@ -125,6 +125,7 @@ describe('TaskList', () => {
const response = { data: { lock_version: 3 } };
jest.spyOn(taskList, 'enableTaskListItems').mockImplementation(() => {});
jest.spyOn(taskList, 'disableTaskListItems').mockImplementation(() => {});
+ jest.spyOn(taskList, 'onUpdate').mockImplementation(() => {});
jest.spyOn(taskList, 'onSuccess').mockImplementation(() => {});
jest.spyOn(axios, 'patch').mockReturnValue(Promise.resolve(response));
@@ -151,8 +152,11 @@ describe('TaskList', () => {
},
};
- taskList
- .update(event)
+ const update = taskList.update(event);
+
+ expect(taskList.onUpdate).toHaveBeenCalled();
+
+ update
.then(() => {
expect(taskList.disableTaskListItems).toHaveBeenCalledWith(event);
expect(axios.patch).toHaveBeenCalledWith(endpoint, patchData);
@@ -168,12 +172,17 @@ describe('TaskList', () => {
it('should handle request error and enable task list items', (done) => {
const response = { data: { error: 1 } };
jest.spyOn(taskList, 'enableTaskListItems').mockImplementation(() => {});
+ jest.spyOn(taskList, 'onUpdate').mockImplementation(() => {});
jest.spyOn(taskList, 'onError').mockImplementation(() => {});
jest.spyOn(axios, 'patch').mockReturnValue(Promise.reject({ response })); // eslint-disable-line prefer-promise-reject-errors
const event = { detail: {} };
- taskList
- .update(event)
+
+ const update = taskList.update(event);
+
+ expect(taskList.onUpdate).toHaveBeenCalled();
+
+ update
.then(() => {
expect(taskList.enableTaskListItems).toHaveBeenCalledWith(event);
expect(taskList.onError).toHaveBeenCalledWith(response.data);
diff --git a/spec/frontend/terms/components/app_spec.js b/spec/frontend/terms/components/app_spec.js
new file mode 100644
index 00000000000..ee78b35843a
--- /dev/null
+++ b/spec/frontend/terms/components/app_spec.js
@@ -0,0 +1,171 @@
+import $ from 'jquery';
+import { merge } from 'lodash';
+import { GlIntersectionObserver } from '@gitlab/ui';
+import { nextTick } from 'vue';
+
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { FLASH_TYPES, FLASH_CLOSED_EVENT } from '~/flash';
+import { isLoggedIn } from '~/lib/utils/common_utils';
+import TermsApp from '~/terms/components/app.vue';
+
+jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
+jest.mock('~/lib/utils/common_utils');
+
+describe('TermsApp', () => {
+ let wrapper;
+ let renderGFMSpy;
+
+ const defaultProvide = {
+ terms: 'foo bar',
+ paths: {
+ accept: '/-/users/terms/1/accept',
+ decline: '/-/users/terms/1/decline',
+ root: '/',
+ },
+ permissions: {
+ canAccept: true,
+ canDecline: true,
+ },
+ };
+
+ const createComponent = (provide = {}) => {
+ wrapper = mountExtended(TermsApp, {
+ provide: merge({}, defaultProvide, provide),
+ });
+ };
+
+ beforeEach(() => {
+ renderGFMSpy = jest.spyOn($.fn, 'renderGFM');
+ isLoggedIn.mockReturnValue(true);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findFormWithAction = (path) => wrapper.find(`form[action="${path}"]`);
+ const findButton = (path) => findFormWithAction(path).find('button[type="submit"]');
+ const findScrollableViewport = () => wrapper.findByTestId('scrollable-viewport');
+
+ const expectFormWithSubmitButton = (buttonText, path) => {
+ const form = findFormWithAction(path);
+ const submitButton = findButton(path);
+
+ expect(form.exists()).toBe(true);
+ expect(submitButton.exists()).toBe(true);
+ expect(submitButton.text()).toBe(buttonText);
+ expect(
+ form
+ .find('input[type="hidden"][name="authenticity_token"][value="mock-csrf-token"]')
+ .exists(),
+ ).toBe(true);
+ };
+
+ it('renders terms of service as markdown', () => {
+ createComponent();
+
+ expect(wrapper.findByText(defaultProvide.terms).exists()).toBe(true);
+ expect(renderGFMSpy).toHaveBeenCalled();
+ });
+
+ describe('accept button', () => {
+ it('is disabled until user scrolls to the bottom of the terms', async () => {
+ createComponent();
+
+ expect(findButton(defaultProvide.paths.accept).attributes('disabled')).toBe('disabled');
+
+ wrapper.find(GlIntersectionObserver).vm.$emit('appear');
+
+ await nextTick();
+
+ expect(findButton(defaultProvide.paths.accept).attributes('disabled')).toBeUndefined();
+ });
+
+ describe('when user has permissions to accept', () => {
+ it('renders form and button to accept terms', () => {
+ createComponent();
+
+ expectFormWithSubmitButton(TermsApp.i18n.accept, defaultProvide.paths.accept);
+ });
+ });
+
+ describe('when user does not have permissions to accept', () => {
+ it('renders continue button', () => {
+ createComponent({ permissions: { canAccept: false } });
+
+ expect(wrapper.findByText(TermsApp.i18n.continue).exists()).toBe(true);
+ });
+ });
+ });
+
+ describe('decline button', () => {
+ describe('when user has permissions to decline', () => {
+ it('renders form and button to decline terms', () => {
+ createComponent();
+
+ expectFormWithSubmitButton(TermsApp.i18n.decline, defaultProvide.paths.decline);
+ });
+ });
+
+ describe('when user does not have permissions to decline', () => {
+ it('does not render decline button', () => {
+ createComponent({ permissions: { canDecline: false } });
+
+ expect(wrapper.findByText(TermsApp.i18n.decline).exists()).toBe(false);
+ });
+ });
+ });
+
+ it('sets height of scrollable viewport', () => {
+ jest.spyOn(document.documentElement, 'scrollHeight', 'get').mockImplementation(() => 800);
+ jest.spyOn(document.documentElement, 'clientHeight', 'get').mockImplementation(() => 600);
+
+ createComponent();
+
+ expect(findScrollableViewport().attributes('style')).toBe('max-height: calc(100vh - 200px);');
+ });
+
+ describe('when flash is closed', () => {
+ let flashEl;
+
+ beforeEach(() => {
+ flashEl = document.createElement('div');
+ flashEl.classList.add(`flash-${FLASH_TYPES.ALERT}`);
+ document.body.appendChild(flashEl);
+ });
+
+ afterEach(() => {
+ document.body.innerHTML = '';
+ });
+
+ it('recalculates height of scrollable viewport', () => {
+ jest.spyOn(document.documentElement, 'scrollHeight', 'get').mockImplementation(() => 800);
+ jest.spyOn(document.documentElement, 'clientHeight', 'get').mockImplementation(() => 600);
+
+ createComponent();
+
+ expect(findScrollableViewport().attributes('style')).toBe('max-height: calc(100vh - 200px);');
+
+ jest.spyOn(document.documentElement, 'scrollHeight', 'get').mockImplementation(() => 700);
+ jest.spyOn(document.documentElement, 'clientHeight', 'get').mockImplementation(() => 600);
+
+ flashEl.dispatchEvent(new Event(FLASH_CLOSED_EVENT));
+
+ expect(findScrollableViewport().attributes('style')).toBe('max-height: calc(100vh - 100px);');
+ });
+ });
+
+ describe('when user is signed out', () => {
+ beforeEach(() => {
+ isLoggedIn.mockReturnValue(false);
+ });
+
+ it('does not show any buttons', () => {
+ createComponent();
+
+ expect(wrapper.findByText(TermsApp.i18n.accept).exists()).toBe(false);
+ expect(wrapper.findByText(TermsApp.i18n.decline).exists()).toBe(false);
+ expect(wrapper.findByText(TermsApp.i18n.continue).exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js
index 2c8e0fff848..40f68c6385f 100644
--- a/spec/frontend/test_setup.js
+++ b/spec/frontend/test_setup.js
@@ -47,10 +47,12 @@ Object.assign(global, {
setFixtures: setHTMLFixture,
});
+const JQUERY_MATCHERS_TO_EXCLUDE = ['toHaveLength', 'toExist'];
+
// custom-jquery-matchers was written for an old Jest version, we need to make it compatible
Object.entries(jqueryMatchers).forEach(([matcherName, matcherFactory]) => {
- // Don't override existing Jest matcher
- if (matcherName === 'toHaveLength') {
+ // Exclude these jQuery matchers
+ if (JQUERY_MATCHERS_TO_EXCLUDE.includes(matcherName)) {
return;
}
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 c9dea4394f9..c2606346292 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,14 +1,20 @@
import { shallowMount } from '@vue/test-utils';
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 {
+ APPROVED_BY_OTHERS,
+ APPROVED_BY_YOU,
+ APPROVED_BY_YOU_AND_OTHERS,
+} from '~/vue_merge_request_widget/components/approvals/messages';
import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue';
+const exampleUserId = 1;
const testApprovers = () => Array.from({ length: 5 }, (_, i) => i).map((id) => ({ id }));
const testRulesLeft = () => ['Lorem', 'Ipsum', 'dolar & sit'];
const TEST_APPROVALS_LEFT = 3;
describe('MRWidget approvals summary', () => {
+ const originalUserId = gon.current_user_id;
let wrapper;
const createComponent = (props = {}) => {
@@ -28,6 +34,7 @@ describe('MRWidget approvals summary', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
+ gon.current_user_id = originalUserId;
});
describe('when approved', () => {
@@ -38,7 +45,7 @@ describe('MRWidget approvals summary', () => {
});
it('shows approved message', () => {
- expect(wrapper.text()).toContain(APPROVED_MESSAGE);
+ expect(wrapper.text()).toContain(APPROVED_BY_OTHERS);
});
it('renders avatar list for approvers', () => {
@@ -51,6 +58,48 @@ describe('MRWidget approvals summary', () => {
}),
);
});
+
+ describe('by the current user', () => {
+ beforeEach(() => {
+ gon.current_user_id = exampleUserId;
+ createComponent({
+ approvers: [{ id: exampleUserId }],
+ approved: true,
+ });
+ });
+
+ it('shows "Approved by you" message', () => {
+ expect(wrapper.text()).toContain(APPROVED_BY_YOU);
+ });
+ });
+
+ describe('by the current user and others', () => {
+ beforeEach(() => {
+ gon.current_user_id = exampleUserId;
+ createComponent({
+ approvers: [{ id: exampleUserId }, { id: exampleUserId + 1 }],
+ approved: true,
+ });
+ });
+
+ it('shows "Approved by you and others" message', () => {
+ expect(wrapper.text()).toContain(APPROVED_BY_YOU_AND_OTHERS);
+ });
+ });
+
+ describe('by other users than the current user', () => {
+ beforeEach(() => {
+ gon.current_user_id = exampleUserId;
+ createComponent({
+ approvers: [{ id: exampleUserId + 1 }],
+ approved: true,
+ });
+ });
+
+ it('shows "Approved by others" message', () => {
+ expect(wrapper.text()).toContain(APPROVED_BY_OTHERS);
+ });
+ });
});
describe('when not approved', () => {
diff --git a/spec/frontend/vue_mr_widget/components/extensions/actions_spec.js b/spec/frontend/vue_mr_widget/components/extensions/actions_spec.js
index d5d779d7a34..a13db2f4d72 100644
--- a/spec/frontend/vue_mr_widget/components/extensions/actions_spec.js
+++ b/spec/frontend/vue_mr_widget/components/extensions/actions_spec.js
@@ -24,6 +24,18 @@ describe('MR widget extension actions', () => {
expect(wrapper.findAllComponents(GlButton)).toHaveLength(1);
});
+ it('calls action click handler', async () => {
+ const onClick = jest.fn();
+
+ factory({
+ tertiaryButtons: [{ text: 'hello world', onClick }],
+ });
+
+ await wrapper.findComponent(GlButton).vm.$emit('click');
+
+ expect(onClick).toHaveBeenCalled();
+ });
+
it('renders tertiary actions in dropdown', () => {
factory({
tertiaryButtons: [{ text: 'hello world', href: 'https://gitlab.com', target: '_blank' }],
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 ecaca16a2cd..6347e3c3be3 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,5 +1,7 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
import { trimText } from 'helpers/text_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue';
@@ -39,6 +41,8 @@ describe('MRWidgetPipeline', () => {
const findMonitoringPipelineMessage = () => wrapper.findByTestId('monitoring-pipeline-message');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const mockArtifactsRequest = () => new MockAdapter(axios).onGet().reply(200, []);
+
const createWrapper = (props = {}, mountFn = shallowMount) => {
wrapper = extendedWrapper(
mountFn(PipelineComponent, {
@@ -71,6 +75,8 @@ describe('MRWidgetPipeline', () => {
describe('with a pipeline', () => {
beforeEach(() => {
+ mockArtifactsRequest();
+
createWrapper(
{
pipelineCoverageDelta: mockData.pipelineCoverageDelta,
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 b5afc1ab21a..8e710b6d65f 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,4 +1,4 @@
-import { GlLink, GlSprintf } from '@gitlab/ui';
+import { GlSprintf } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { mockTracking, triggerEvent, unmockTracking } from 'helpers/tracking_helper';
@@ -7,9 +7,7 @@ import MrWidgetIcon from '~/vue_merge_request_widget/components/mr_widget_icon.v
import suggestPipelineComponent from '~/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue';
import {
SP_TRACK_LABEL,
- SP_LINK_TRACK_EVENT,
SP_SHOW_TRACK_EVENT,
- SP_LINK_TRACK_VALUE,
SP_SHOW_TRACK_VALUE,
SP_HELP_URL,
} from '~/vue_merge_request_widget/constants';
@@ -52,15 +50,8 @@ describe('MRWidgetSuggestPipeline', () => {
mockAxios.restore();
});
- it('renders add pipeline file link', () => {
- const link = wrapper.find(GlLink);
-
- expect(link.exists()).toBe(true);
- expect(link.attributes().href).toBe(suggestProps.pipelinePath);
- });
-
it('renders the expected text', () => {
- const messageText = /\s*No pipeline\s*Add the .gitlab-ci.yml file\s*to create one./;
+ const messageText = /Looks like there's no pipeline here./;
expect(wrapper.text()).toMatch(messageText);
});
@@ -109,18 +100,6 @@ describe('MRWidgetSuggestPipeline', () => {
});
});
- it('send an event when add pipeline link is clicked', () => {
- mockTrackingOnWrapper();
- const link = wrapper.find('[data-testid="add-pipeline-link"]');
- triggerEvent(link.element);
-
- expect(trackingSpy).toHaveBeenCalledWith('_category_', SP_LINK_TRACK_EVENT, {
- label: SP_TRACK_LABEL,
- property: suggestProps.humanAccess,
- value: SP_LINK_TRACK_VALUE.toString(),
- });
- });
-
it('send an event when ok button is clicked', () => {
mockTrackingOnWrapper();
const okBtn = findOkBtn();
diff --git a/spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap b/spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap
index 5981d2d7849..56a0218b374 100644
--- a/spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap
+++ b/spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap
@@ -50,7 +50,7 @@ exports[`MRWidgetAutoMergeEnabled when graphql is disabled template should have
<span
class="gl-mr-3"
>
- The source branch will not be deleted
+ Does not delete the source branch
</span>
<gl-button-stub
@@ -122,7 +122,7 @@ exports[`MRWidgetAutoMergeEnabled when graphql is enabled template should have c
<span
class="gl-mr-3"
>
- The source branch will not be deleted
+ Does not delete the source branch
</span>
<gl-button-stub
diff --git a/spec/frontend/vue_mr_widget/components/states/__snapshots__/new_ready_to_merge_spec.js.snap b/spec/frontend/vue_mr_widget/components/states/__snapshots__/new_ready_to_merge_spec.js.snap
index a6c36764c41..f9936f22ea3 100644
--- a/spec/frontend/vue_mr_widget/components/states/__snapshots__/new_ready_to_merge_spec.js.snap
+++ b/spec/frontend/vue_mr_widget/components/states/__snapshots__/new_ready_to_merge_spec.js.snap
@@ -9,7 +9,7 @@ exports[`New ready to merge state component renders permission text if canMerge
/>
<p
- class="media-body gl-m-0! gl-font-weight-bold"
+ class="media-body gl-m-0! gl-font-weight-bold gl-text-gray-900!"
>
Ready to merge by members who can write to the target branch.
@@ -27,7 +27,7 @@ exports[`New ready to merge state component renders permission text if canMerge
/>
<p
- class="media-body gl-m-0! gl-font-weight-bold"
+ class="media-body gl-m-0! gl-font-weight-bold gl-text-gray-900!"
>
Ready to merge!
diff --git a/spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js b/spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js
index 8214cedc4a1..f965fc32dc1 100644
--- a/spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js
@@ -3,6 +3,7 @@ import CommitEdit from '~/vue_merge_request_widget/components/states/commit_edit
const testCommitMessage = 'Test commit message';
const testLabel = 'Test label';
+const testTextMuted = 'Test text muted';
const testInputId = 'test-input-id';
describe('Commits edit component', () => {
@@ -63,7 +64,7 @@ describe('Commits edit component', () => {
beforeEach(() => {
createComponent({
header: `<div class="test-header">${testCommitMessage}</div>`,
- checkbox: `<label class="test-checkbox">${testLabel}</label >`,
+ 'text-muted': `<p class="test-text-muted">${testTextMuted}</p>`,
});
});
@@ -74,11 +75,11 @@ describe('Commits edit component', () => {
expect(headerSlotElement.text()).toBe(testCommitMessage);
});
- it('renders checkbox slot correctly', () => {
- const checkboxSlotElement = wrapper.find('.test-checkbox');
+ it('renders text-muted slot correctly', () => {
+ const textMutedElement = wrapper.find('.test-text-muted');
- expect(checkboxSlotElement.exists()).toBe(true);
- expect(checkboxSlotElement.text()).toBe(testLabel);
+ expect(textMutedElement.exists()).toBe(true);
+ expect(textMutedElement.text()).toBe(testTextMuted);
});
});
});
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 4c1534574f5..d0a6af9970e 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
@@ -270,8 +270,8 @@ describe('MRWidgetAutoMergeEnabled', () => {
const normalizedText = wrapper.text().replace(/\s+/g, ' ');
- expect(normalizedText).toContain('The source branch will be deleted');
- expect(normalizedText).not.toContain('The source branch will not be deleted');
+ expect(normalizedText).toContain('Deletes the source branch');
+ expect(normalizedText).not.toContain('Does not delete the source branch');
});
it('should not show delete source branch button when user not able to delete source branch', () => {
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js
index 2ff94a547f4..5858654e518 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js
@@ -1,4 +1,4 @@
-import { shallowMount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import { GlSprintf } from '@gitlab/ui';
import CommitsHeader from '~/vue_merge_request_widget/components/states/commits_header.vue';
@@ -6,7 +6,7 @@ describe('Commits header component', () => {
let wrapper;
const createComponent = (props) => {
- wrapper = shallowMount(CommitsHeader, {
+ wrapper = mount(CommitsHeader, {
stubs: {
GlSprintf,
},
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 9c3a6d581e8..e0f1f091129 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
@@ -191,7 +191,7 @@ describe('MRWidgetMerged', () => {
});
it('shows button to copy commit SHA to clipboard', () => {
- expect(selectors.copyMergeShaButton).toExist();
+ expect(selectors.copyMergeShaButton).not.toBe(null);
expect(selectors.copyMergeShaButton.getAttribute('data-clipboard-text')).toBe(
vm.mr.mergeCommitSha,
);
@@ -201,14 +201,14 @@ describe('MRWidgetMerged', () => {
vm.mr.mergeCommitSha = null;
Vue.nextTick(() => {
- expect(selectors.copyMergeShaButton).not.toExist();
+ expect(selectors.copyMergeShaButton).toBe(null);
expect(vm.$el.querySelector('.mr-info-list').innerText).not.toContain('with');
done();
});
});
it('shows merge commit SHA link', () => {
- expect(selectors.mergeCommitShaLink).toExist();
+ expect(selectors.mergeCommitShaLink).not.toBe(null);
expect(selectors.mergeCommitShaLink.text).toContain(vm.mr.shortMergeCommitSha);
expect(selectors.mergeCommitShaLink.href).toBe(vm.mr.mergeCommitPath);
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_merging_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_merging_spec.js
index b6c16958993..e6b2e9fa176 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_merging_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_merging_spec.js
@@ -42,7 +42,7 @@ describe('MRWidgetMerging', () => {
.trim()
.replace(/\s\s+/g, ' ')
.replace(/[\r\n]+/g, ' '),
- ).toEqual('The changes will be merged into branch');
+ ).toEqual('Merges changes into branch');
expect(wrapper.find('a').attributes('href')).toBe('/branch-path');
});
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 f0fbb1d5851..016b6b2220b 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
@@ -269,19 +269,6 @@ describe('ReadyToMerge', () => {
});
describe('methods', () => {
- describe('updateMergeCommitMessage', () => {
- it('should revert flag and change commitMessage', () => {
- createComponent();
-
- wrapper.vm.updateMergeCommitMessage(true);
-
- expect(wrapper.vm.commitMessage).toEqual(commitMessageWithDescription);
- wrapper.vm.updateMergeCommitMessage(false);
-
- expect(wrapper.vm.commitMessage).toEqual(commitMessage);
- });
- });
-
describe('handleMergeButtonClick', () => {
const returnPromise = (status) =>
new Promise((resolve) => {
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 8ead0002950..6abdbd11f5e 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,4 +1,4 @@
-import { GlFormCheckbox } from '@gitlab/ui';
+import { GlFormCheckbox, GlLink } 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';
@@ -77,7 +77,7 @@ describe('Squash before merge component', () => {
value: false,
});
- const aboutLink = wrapper.find('a');
+ const aboutLink = wrapper.findComponent(GlLink);
expect(aboutLink.exists()).toBe(false);
});
@@ -88,7 +88,7 @@ describe('Squash before merge component', () => {
helpPath: 'test-path',
});
- const aboutLink = wrapper.find('a');
+ const aboutLink = wrapper.findComponent(GlLink);
expect(aboutLink.exists()).toBe(true);
});
@@ -99,7 +99,7 @@ describe('Squash before merge component', () => {
helpPath: 'test-path',
});
- const aboutLink = wrapper.find('a');
+ const aboutLink = wrapper.findComponent(GlLink);
expect(aboutLink.attributes('href')).toEqual('test-path');
});
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 be15e4df66d..0fb0d5b0b68 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
@@ -46,7 +46,7 @@ describe('Wip', () => {
is_new_mr_data: true,
};
- describe('handleRemoveWIP', () => {
+ describe('handleRemoveDraft', () => {
it('should make a request to service and handle response', (done) => {
const vm = createComponent();
@@ -59,7 +59,7 @@ describe('Wip', () => {
}),
);
- vm.handleRemoveWIP();
+ vm.handleRemoveDraft();
setImmediate(() => {
expect(vm.isMakingRequest).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
@@ -84,7 +84,7 @@ describe('Wip', () => {
expect(el.innerText).toContain('This merge request is still a draft.');
expect(el.querySelector('button').getAttribute('disabled')).toBeTruthy();
expect(el.querySelector('button').innerText).toContain('Merge');
- expect(el.querySelector('.js-remove-wip').innerText.replace(/\s\s+/g, ' ')).toContain(
+ expect(el.querySelector('.js-remove-draft').innerText.replace(/\s\s+/g, ' ')).toContain(
'Mark as ready',
);
});
@@ -93,7 +93,7 @@ describe('Wip', () => {
vm.mr.removeWIPPath = '';
Vue.nextTick(() => {
- expect(el.querySelector('.js-remove-wip')).toEqual(null);
+ expect(el.querySelector('.js-remove-draft')).toEqual(null);
done();
});
});
diff --git a/spec/frontend/vue_mr_widget/mock_data.js b/spec/frontend/vue_mr_widget/mock_data.js
index 34a741cf8f2..f0c1da346a1 100644
--- a/spec/frontend/vue_mr_widget/mock_data.js
+++ b/spec/frontend/vue_mr_widget/mock_data.js
@@ -51,7 +51,7 @@ export default {
target_branch: 'main',
target_project_id: 19,
target_project_full_path: '/group2/project2',
- merge_request_add_ci_config_path: '/group2/project2/new/pipeline',
+ merge_request_add_ci_config_path: '/root/group2/project2/-/ci/editor',
is_dismissed_suggest_pipeline: false,
user_callouts_path: 'some/callout/path',
suggest_pipeline_feature_id: 'suggest_pipeline',
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 5aba6982886..550f156d095 100644
--- a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
@@ -1,4 +1,4 @@
-import { GlBadge, GlLink, GlIcon } from '@gitlab/ui';
+import { GlBadge, GlLink, GlIcon, GlButton, GlDropdown } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
@@ -6,6 +6,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { securityReportMergeRequestDownloadPathsQueryResponse } from 'jest/vue_shared/security_reports/mock_data';
+import api from '~/api';
import axios from '~/lib/utils/axios_utils';
import { setFaviconOverlay } from '~/lib/utils/favicon';
import notify from '~/lib/utils/notify';
@@ -23,6 +24,8 @@ import { faviconDataUrl, overlayDataUrl } from '../lib/utils/mock_data';
import mockData from './mock_data';
import testExtension from './test_extension';
+jest.mock('~/api.js');
+
jest.mock('~/smart_interval');
jest.mock('~/lib/utils/favicon');
@@ -540,7 +543,7 @@ describe('MrWidgetOptions', () => {
nextTick(() => {
const tooltip = wrapper.find('[data-testid="question-o-icon"]');
- expect(wrapper.text()).toContain('The source branch will be deleted');
+ expect(wrapper.text()).toContain('Deletes the source branch');
expect(tooltip.attributes('title')).toBe(
'A user with write access to the source branch selected this option',
);
@@ -556,7 +559,7 @@ describe('MrWidgetOptions', () => {
nextTick(() => {
expect(wrapper.text()).toContain('The source branch has been deleted');
- expect(wrapper.text()).not.toContain('The source branch will be deleted');
+ expect(wrapper.text()).not.toContain('Deletes the source branch');
done();
});
@@ -904,6 +907,18 @@ describe('MrWidgetOptions', () => {
expect(wrapper.text()).toContain('Test extension summary count: 1');
});
+ it('triggers trackRedisHllUserEvent API call', async () => {
+ await waitForPromises();
+
+ wrapper
+ .find('[data-testid="widget-extension"] [data-testid="toggle-button"]')
+ .trigger('click');
+
+ await Vue.nextTick();
+
+ expect(api.trackRedisHllUserEvent).toHaveBeenCalledWith('test_expand_event');
+ });
+
it('renders full data', async () => {
await waitForPromises();
@@ -913,6 +928,10 @@ describe('MrWidgetOptions', () => {
await Vue.nextTick();
+ expect(
+ wrapper.find('[data-testid="widget-extension-top-level"]').find(GlDropdown).exists(),
+ ).toBe(false);
+
const collapsedSection = wrapper.find('[data-testid="widget-extension-collapsed-section"]');
expect(collapsedSection.exists()).toBe(true);
expect(collapsedSection.text()).toContain('Hello world');
@@ -928,6 +947,9 @@ describe('MrWidgetOptions', () => {
// Renders a link in the row
expect(collapsedSection.find(GlLink).exists()).toBe(true);
expect(collapsedSection.find(GlLink).text()).toBe('GitLab.com');
+
+ expect(collapsedSection.find(GlButton).exists()).toBe(true);
+ expect(collapsedSection.find(GlButton).text()).toBe('Full report');
});
});
});
diff --git a/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js b/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js
index 631d4647b17..fc760f5c5be 100644
--- a/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js
+++ b/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js
@@ -15,7 +15,7 @@ describe('getStateKey', () => {
branchMissing: false,
commitsCount: 2,
hasConflicts: false,
- workInProgress: false,
+ draft: false,
};
const bound = getStateKey.bind(context);
@@ -49,9 +49,9 @@ describe('getStateKey', () => {
expect(bound()).toEqual('unresolvedDiscussions');
- context.workInProgress = true;
+ context.draft = true;
- expect(bound()).toEqual('workInProgress');
+ expect(bound()).toEqual('draft');
context.onlyAllowMergeIfPipelineSucceeds = true;
context.isPipelineFailed = true;
@@ -74,6 +74,7 @@ describe('getStateKey', () => {
expect(bound()).toEqual('nothingToMerge');
+ context.commitsCount = 1;
context.branchMissing = true;
expect(bound()).toEqual('missingBranch');
@@ -98,7 +99,7 @@ describe('getStateKey', () => {
branchMissing: false,
commitsCount: 2,
hasConflicts: false,
- workInProgress: false,
+ draft: false,
};
const bound = getStateKey.bind(context);
diff --git a/spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js b/spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js
index febcfcd4019..6eb68a1b00d 100644
--- a/spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js
+++ b/spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js
@@ -129,7 +129,7 @@ describe('MergeRequestStore', () => {
it('should set the add ci config path', () => {
store.setPaths({ ...mockData });
- expect(store.mergeRequestAddCiConfigPath).toBe('/group2/project2/new/pipeline');
+ expect(store.mergeRequestAddCiConfigPath).toBe('/root/group2/project2/-/ci/editor');
});
it('should set humanAccess=Maintainer when user has that role', () => {
diff --git a/spec/frontend/vue_mr_widget/test_extension.js b/spec/frontend/vue_mr_widget/test_extension.js
index a29a4d2fb46..65c1bd8473b 100644
--- a/spec/frontend/vue_mr_widget/test_extension.js
+++ b/spec/frontend/vue_mr_widget/test_extension.js
@@ -3,6 +3,7 @@ import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants';
export default {
name: 'WidgetTestExtension',
props: ['targetProjectFullPath'],
+ expandEvent: 'test_expand_event',
computed: {
summary({ count, targetProjectFullPath }) {
return `Test extension summary count: ${count} & ${targetProjectFullPath}`;
@@ -30,6 +31,7 @@ export default {
href: 'https://gitlab.com',
text: 'GitLab.com',
},
+ actions: [{ text: 'Full report', href: 'https://gitlab.com', target: '_blank' }],
},
]);
},
diff --git a/spec/frontend/vue_shared/components/alerts_deprecation_warning_spec.js b/spec/frontend/vue_shared/components/alerts_deprecation_warning_spec.js
deleted file mode 100644
index b73f4d6a396..00000000000
--- a/spec/frontend/vue_shared/components/alerts_deprecation_warning_spec.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import { GlAlert, GlLink } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import AlertDeprecationWarning from '~/vue_shared/components/alerts_deprecation_warning.vue';
-
-describe('AlertDetails', () => {
- let wrapper;
-
- function mountComponent(hasManagedPrometheus = false) {
- wrapper = mount(AlertDeprecationWarning, {
- provide: {
- hasManagedPrometheus,
- },
- });
- }
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- const findAlert = () => wrapper.findComponent(GlAlert);
- const findLink = () => wrapper.findComponent(GlLink);
-
- describe('Alert details', () => {
- describe('with no manual prometheus', () => {
- beforeEach(() => {
- mountComponent();
- });
-
- it('renders nothing', () => {
- expect(findAlert().exists()).toBe(false);
- });
- });
-
- describe('with manual prometheus', () => {
- beforeEach(() => {
- mountComponent(true);
- });
-
- it('renders a deprecation notice', () => {
- expect(findAlert().text()).toContain('GitLab-managed Prometheus is deprecated');
- expect(findLink().attributes('href')).toContain(
- 'operations/metrics/alerts.html#managed-prometheus-instances',
- );
- });
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js
new file mode 100644
index 00000000000..f75694bd504
--- /dev/null
+++ b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js
@@ -0,0 +1,99 @@
+import { GlModal, GlSprintf } from '@gitlab/ui';
+import {
+ CONFIRM_DANGER_WARNING,
+ CONFIRM_DANGER_MODAL_BUTTON,
+ CONFIRM_DANGER_MODAL_ID,
+} from '~/vue_shared/components/confirm_danger/constants';
+import ConfirmDangerModal from '~/vue_shared/components/confirm_danger/confirm_danger_modal.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('Confirm Danger Modal', () => {
+ const confirmDangerMessage = 'This is a dangerous activity';
+ const confirmButtonText = 'Confirm button text';
+ const phrase = 'You must construct additional pylons';
+ const modalId = CONFIRM_DANGER_MODAL_ID;
+
+ let wrapper;
+
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findConfirmationPhrase = () => wrapper.findByTestId('confirm-danger-phrase');
+ const findConfirmationInput = () => wrapper.findByTestId('confirm-danger-input');
+ const findDefaultWarning = () => wrapper.findByTestId('confirm-danger-warning');
+ const findAdditionalMessage = () => wrapper.findByTestId('confirm-danger-message');
+ const findPrimaryAction = () => findModal().props('actionPrimary');
+ const findPrimaryActionAttributes = (attr) => findPrimaryAction().attributes[0][attr];
+
+ const createComponent = ({ provide = {} } = {}) =>
+ shallowMountExtended(ConfirmDangerModal, {
+ propsData: {
+ modalId,
+ phrase,
+ },
+ provide,
+ stubs: { GlSprintf },
+ });
+
+ beforeEach(() => {
+ wrapper = createComponent({ provide: { confirmDangerMessage, confirmButtonText } });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the default warning message', () => {
+ expect(findDefaultWarning().text()).toBe(CONFIRM_DANGER_WARNING);
+ });
+
+ it('renders any additional messages', () => {
+ expect(findAdditionalMessage().text()).toBe(confirmDangerMessage);
+ });
+
+ it('renders the confirm button', () => {
+ expect(findPrimaryAction().text).toBe(confirmButtonText);
+ expect(findPrimaryActionAttributes('variant')).toBe('danger');
+ });
+
+ it('renders the correct confirmation phrase', () => {
+ expect(findConfirmationPhrase().text()).toBe(
+ `Please type ${phrase} to proceed or close this modal to cancel.`,
+ );
+ });
+
+ describe('without injected data', () => {
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ it('does not render any additional messages', () => {
+ expect(findAdditionalMessage().exists()).toBe(false);
+ });
+
+ it('renders the default confirm button', () => {
+ expect(findPrimaryAction().text).toBe(CONFIRM_DANGER_MODAL_BUTTON);
+ });
+ });
+
+ describe('with a valid confirmation phrase', () => {
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ it('enables the confirm button', async () => {
+ expect(findPrimaryActionAttributes('disabled')).toBe(true);
+
+ await findConfirmationInput().vm.$emit('input', phrase);
+
+ expect(findPrimaryActionAttributes('disabled')).toBe(false);
+ });
+
+ it('emits a `confirm` event when the button is clicked', async () => {
+ expect(wrapper.emitted('confirm')).toBeUndefined();
+
+ await findConfirmationInput().vm.$emit('input', phrase);
+ await findModal().vm.$emit('primary');
+
+ expect(wrapper.emitted('confirm')).not.toBeUndefined();
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js
new file mode 100644
index 00000000000..220f897c035
--- /dev/null
+++ b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js
@@ -0,0 +1,61 @@
+import { GlButton } from '@gitlab/ui';
+import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
+import ConfirmDangerModal from '~/vue_shared/components/confirm_danger/confirm_danger_modal.vue';
+import { CONFIRM_DANGER_MODAL_ID } from '~/vue_shared/components/confirm_danger/constants';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('Confirm Danger Modal', () => {
+ let wrapper;
+
+ const phrase = 'En Taro Adun';
+ const buttonText = 'Click me!';
+ const modalId = CONFIRM_DANGER_MODAL_ID;
+
+ const findBtn = () => wrapper.findComponent(GlButton);
+ const findModal = () => wrapper.findComponent(ConfirmDangerModal);
+ const findModalProps = () => findModal().props();
+
+ const createComponent = (props = {}) =>
+ shallowMountExtended(ConfirmDanger, {
+ propsData: {
+ buttonText,
+ phrase,
+ ...props,
+ },
+ });
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the button', () => {
+ expect(wrapper.html()).toContain(buttonText);
+ });
+
+ it('sets the modal properties', () => {
+ expect(findModalProps()).toMatchObject({
+ modalId,
+ phrase,
+ });
+ });
+
+ it('will disable the button if `disabled=true`', () => {
+ expect(findBtn().attributes('disabled')).toBeUndefined();
+
+ wrapper = createComponent({ disabled: true });
+
+ expect(findBtn().attributes('disabled')).toBe('true');
+ });
+
+ it('will emit `confirm` when the modal confirms', () => {
+ expect(wrapper.emitted('confirm')).toBeUndefined();
+
+ findModal().vm.$emit('confirm');
+
+ expect(wrapper.emitted('confirm')).not.toBeUndefined();
+ });
+});
diff --git a/spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js b/spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js
index 16e7e4dd5cc..f28805471f8 100644
--- a/spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js
@@ -16,6 +16,6 @@ describe('ContentViewer', () => {
propsData: { path, fileSize: 1024, type },
});
- expect(wrapper.find(selector).element).toExist();
+ expect(wrapper.find(selector).exists()).toBe(true);
});
});
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 3ffb23dc7a0..1397fb0405e 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
@@ -42,7 +42,7 @@ describe('MarkdownViewer', () => {
it('renders an animation container while the markdown is loading', () => {
createComponent();
- expect(wrapper.find('.animation-container')).toExist();
+ expect(wrapper.find('.animation-container').exists()).toBe(true);
});
it('renders markdown preview preview renders and loads rendered markdown from server', () => {
diff --git a/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js
index 016fe1f131e..b3af5fd3feb 100644
--- a/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js
+++ b/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js
@@ -34,6 +34,7 @@ describe('DropdownWidget component', () => {
// invokes `show` method of BDropdown used inside GlDropdown.
// Context: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54895#note_524281679
jest.spyOn(wrapper.vm, 'showDropdown').mockImplementation();
+ jest.spyOn(findDropdown().vm, 'hide').mockImplementation();
};
beforeEach(() => {
@@ -67,10 +68,7 @@ describe('DropdownWidget component', () => {
});
it('emits set-option event when clicking on an option', async () => {
- wrapper
- .findAll('[data-testid="unselected-option"]')
- .at(1)
- .vm.$emit('click', new Event('click'));
+ wrapper.findAll('[data-testid="unselected-option"]').at(1).trigger('click');
await wrapper.vm.$nextTick();
expect(wrapper.emitted('set-option')).toEqual([[wrapper.props().options[1]]]);
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 8e931aebfe0..64d15884333 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
@@ -25,6 +25,7 @@ import {
tokenValueMilestone,
tokenValueMembership,
tokenValueConfidential,
+ tokenValueEmpty,
} from './mock_data';
jest.mock('~/vue_shared/components/filtered_search_bar/filtered_search_utils', () => ({
@@ -43,6 +44,7 @@ const createComponent = ({
recentSearchesStorageKey = 'requirements',
tokens = mockAvailableTokens,
sortOptions,
+ initialFilterValue = [],
showCheckbox = false,
checkboxChecked = false,
searchInputPlaceholder = 'Filter requirements',
@@ -55,6 +57,7 @@ const createComponent = ({
recentSearchesStorageKey,
tokens,
sortOptions,
+ initialFilterValue,
showCheckbox,
checkboxChecked,
searchInputPlaceholder,
@@ -193,19 +196,27 @@ describe('FilteredSearchBarRoot', () => {
describe('watchers', () => {
describe('filterValue', () => {
- it('emits component event `onFilter` with empty array when `filterValue` is cleared by GlFilteredSearch', () => {
+ it('emits component event `onFilter` with empty array and false when filter was never selected', () => {
+ wrapper = createComponent({ initialFilterValue: [tokenValueEmpty] });
wrapper.setData({
initialRender: false,
- filterValue: [
- {
- type: 'filtered-search-term',
- value: { data: '' },
- },
- ],
+ filterValue: [tokenValueEmpty],
+ });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.emitted('onFilter')[0]).toEqual([[], false]);
+ });
+ });
+
+ it('emits component event `onFilter` with empty array and true when initially selected filter value was cleared', () => {
+ wrapper = createComponent({ initialFilterValue: [tokenValueLabel] });
+ wrapper.setData({
+ initialRender: false,
+ filterValue: [tokenValueEmpty],
});
return wrapper.vm.$nextTick(() => {
- expect(wrapper.emitted('onFilter')[0]).toEqual([[]]);
+ expect(wrapper.emitted('onFilter')[0]).toEqual([[], true]);
});
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
index ae02c554e13..238c5d16db5 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
@@ -9,6 +9,7 @@ import EpicToken from '~/vue_shared/components/filtered_search_bar/tokens/epic_t
import IterationToken from '~/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
+import ReleaseToken from '~/vue_shared/components/filtered_search_bar/tokens/release_token.vue';
import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue';
export const mockAuthor1 = {
@@ -110,6 +111,18 @@ export const mockIterationToken = {
fetchIterations: () => Promise.resolve(),
};
+export const mockIterations = [
+ {
+ id: 1,
+ title: 'Iteration 1',
+ startDate: '2021-11-05',
+ dueDate: '2021-11-10',
+ iterationCadence: {
+ title: 'Cadence 1',
+ },
+ },
+];
+
export const mockLabelToken = {
type: 'label_name',
icon: 'labels',
@@ -132,6 +145,14 @@ export const mockMilestoneToken = {
fetchMilestones: () => Promise.resolve({ data: mockMilestones }),
};
+export const mockReleaseToken = {
+ type: 'release',
+ icon: 'rocket',
+ title: 'Release',
+ token: ReleaseToken,
+ fetchReleases: () => Promise.resolve(),
+};
+
export const mockEpicToken = {
type: 'epic_iid',
icon: 'clock',
@@ -282,6 +303,11 @@ export const tokenValuePlain = {
value: { data: 'foo' },
};
+export const tokenValueEmpty = {
+ type: 'filtered-search-term',
+ value: { data: '' },
+};
+
export const tokenValueEpic = {
type: 'epic_iid',
value: {
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 14fcffd3c50..b29c394e7ae 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
@@ -112,6 +112,35 @@ describe('AuthorToken', () => {
});
});
+ // TODO: rm when completed https://gitlab.com/gitlab-org/gitlab/-/issues/345756
+ describe('when there are null users presents', () => {
+ const mockAuthorsWithNullUser = mockAuthors.concat([null]);
+
+ beforeEach(() => {
+ jest
+ .spyOn(wrapper.vm.config, 'fetchAuthors')
+ .mockResolvedValue({ data: mockAuthorsWithNullUser });
+
+ getBaseToken().vm.$emit('fetch-suggestions', 'root');
+ });
+
+ describe('when res.data is present', () => {
+ it('filters the successful response when null values are present', () => {
+ return waitForPromises().then(() => {
+ expect(getBaseToken().props('suggestions')).toEqual(mockAuthors);
+ });
+ });
+ });
+
+ describe('when response is an array', () => {
+ it('filters the successful response when null values are present', () => {
+ return waitForPromises().then(() => {
+ expect(getBaseToken().props('suggestions')).toEqual(mockAuthors);
+ });
+ });
+ });
+ });
+
it('calls `createFlash` with flash error message when request fails', () => {
jest.spyOn(wrapper.vm.config, 'fetchAuthors').mockRejectedValue({});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js
index af90ee93543..44bc16adb97 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js
@@ -1,9 +1,13 @@
-import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui';
+import {
+ GlFilteredSearchToken,
+ GlFilteredSearchTokenSegment,
+ GlFilteredSearchSuggestion,
+} from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import IterationToken from '~/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue';
-import { mockIterationToken } from '../mock_data';
+import { mockIterationToken, mockIterations } from '../mock_data';
jest.mock('~/flash');
@@ -11,10 +15,16 @@ describe('IterationToken', () => {
const id = 123;
let wrapper;
- const createComponent = ({ config = mockIterationToken, value = { data: '' } } = {}) =>
+ const createComponent = ({
+ config = mockIterationToken,
+ value = { data: '' },
+ active = false,
+ stubs = {},
+ provide = {},
+ } = {}) =>
mount(IterationToken, {
propsData: {
- active: false,
+ active,
config,
value,
},
@@ -22,13 +32,39 @@ describe('IterationToken', () => {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
suggestionsListClass: () => 'custom-class',
+ ...provide,
},
+ stubs,
});
afterEach(() => {
wrapper.destroy();
});
+ describe('when iteration cadence feature is available', () => {
+ beforeEach(async () => {
+ wrapper = createComponent({
+ active: true,
+ config: { ...mockIterationToken, initialIterations: mockIterations },
+ value: { data: 'i' },
+ stubs: { Portal: true },
+ provide: {
+ glFeatures: {
+ iterationCadences: true,
+ },
+ },
+ });
+
+ await wrapper.setData({ loading: false });
+ });
+
+ it('renders iteration start date and due date', () => {
+ const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
+
+ expect(suggestions.at(3).text()).toContain('Nov 5, 2021 - Nov 10, 2021');
+ });
+ });
+
it('renders iteration value', async () => {
wrapper = createComponent({ value: { data: id } });
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js
new file mode 100644
index 00000000000..b804ff97b82
--- /dev/null
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js
@@ -0,0 +1,78 @@
+import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
+import ReleaseToken from '~/vue_shared/components/filtered_search_bar/tokens/release_token.vue';
+import { mockReleaseToken } from '../mock_data';
+
+jest.mock('~/flash');
+
+describe('ReleaseToken', () => {
+ const id = 123;
+ let wrapper;
+
+ const createComponent = ({ config = mockReleaseToken, value = { data: '' } } = {}) =>
+ mount(ReleaseToken, {
+ propsData: {
+ active: false,
+ config,
+ value,
+ },
+ provide: {
+ portalName: 'fake target',
+ alignSuggestions: function fakeAlignSuggestions() {},
+ suggestionsListClass: () => 'custom-class',
+ },
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders release value', async () => {
+ wrapper = createComponent({ value: { data: id } });
+ await wrapper.vm.$nextTick();
+
+ const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
+
+ expect(tokenSegments).toHaveLength(3); // `Release` `=` `v1`
+ expect(tokenSegments.at(2).text()).toBe(id.toString());
+ });
+
+ it('fetches initial values', () => {
+ const fetchReleasesSpy = jest.fn().mockResolvedValue();
+
+ wrapper = createComponent({
+ config: { ...mockReleaseToken, fetchReleases: fetchReleasesSpy },
+ value: { data: id },
+ });
+
+ expect(fetchReleasesSpy).toHaveBeenCalledWith(id);
+ });
+
+ it('fetches releases on user input', () => {
+ const search = 'hello';
+ const fetchReleasesSpy = jest.fn().mockResolvedValue();
+
+ wrapper = createComponent({
+ config: { ...mockReleaseToken, fetchReleases: fetchReleasesSpy },
+ });
+
+ wrapper.findComponent(GlFilteredSearchToken).vm.$emit('input', { data: search });
+
+ expect(fetchReleasesSpy).toHaveBeenCalledWith(search);
+ });
+
+ it('renders error message when request fails', async () => {
+ const fetchReleasesSpy = jest.fn().mockRejectedValue();
+
+ wrapper = createComponent({
+ config: { ...mockReleaseToken, fetchReleases: fetchReleasesSpy },
+ });
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'There was a problem fetching releases.',
+ });
+ });
+});
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 42f4439df51..b76f475a6fb 100644
--- a/spec/frontend/vue_shared/components/header_ci_component_spec.js
+++ b/spec/frontend/vue_shared/components/header_ci_component_spec.js
@@ -1,4 +1,4 @@
-import { GlButton, GlLink } from '@gitlab/ui';
+import { GlButton, GlAvatarLink } 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';
@@ -18,6 +18,7 @@ describe('Header CI Component', () => {
},
time: '2017-05-08T14:57:39.781Z',
user: {
+ id: 1234,
web_url: 'path',
name: 'Foo',
username: 'foobar',
@@ -29,7 +30,7 @@ describe('Header CI Component', () => {
const findIconBadge = () => wrapper.findComponent(CiIconBadge);
const findTimeAgo = () => wrapper.findComponent(TimeagoTooltip);
- const findUserLink = () => wrapper.findComponent(GlLink);
+ const findUserLink = () => wrapper.findComponent(GlAvatarLink);
const findSidebarToggleBtn = () => wrapper.findComponent(GlButton);
const findActionButtons = () => wrapper.findByTestId('ci-header-action-buttons');
const findHeaderItemText = () => wrapper.findByTestId('ci-header-item-text');
@@ -64,10 +65,6 @@ describe('Header CI Component', () => {
expect(findTimeAgo().exists()).toBe(true);
});
- it('should render user icon and name', () => {
- expect(findUserLink().text()).toContain(defaultProps.user.name);
- });
-
it('should render sidebar toggle button', () => {
expect(findSidebarToggleBtn().exists()).toBe(true);
});
@@ -77,6 +74,45 @@ describe('Header CI Component', () => {
});
});
+ describe('user avatar', () => {
+ beforeEach(() => {
+ createComponent({ itemName: 'Pipeline' });
+ });
+
+ it('contains the username', () => {
+ expect(findUserLink().text()).toContain(defaultProps.user.username);
+ });
+
+ it('has the correct data attributes', () => {
+ expect(findUserLink().attributes()).toMatchObject({
+ 'data-user-id': defaultProps.user.id.toString(),
+ 'data-username': defaultProps.user.username,
+ 'data-name': defaultProps.user.name,
+ });
+ });
+
+ describe('with data from GraphQL', () => {
+ const userId = 1;
+
+ beforeEach(() => {
+ createComponent({
+ itemName: 'Pipeline',
+ user: { ...defaultProps.user, id: `gid://gitlab/User/${1}` },
+ });
+ });
+
+ it('has the correct user id', () => {
+ expect(findUserLink().attributes('data-user-id')).toBe(userId.toString());
+ });
+ });
+
+ describe('with data from REST', () => {
+ it('has the correct user id', () => {
+ expect(findUserLink().attributes('data-user-id')).toBe(defaultProps.user.id.toString());
+ });
+ });
+ });
+
describe('with item id', () => {
beforeEach(() => {
createComponent({ itemName: 'Pipeline', itemId: '123' });
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 48dacc50923..65f79bab005 100644
--- a/spec/frontend/vue_shared/components/notes/system_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/system_note_spec.js
@@ -1,13 +1,27 @@
+import MockAdapter from 'axios-mock-adapter';
import { mount } from '@vue/test-utils';
+import waitForPromises from 'helpers/wait_for_promises';
import initMRPopovers from '~/mr_popover/index';
import createStore from '~/notes/stores';
import IssueSystemNote from '~/vue_shared/components/notes/system_note.vue';
+import axios from '~/lib/utils/axios_utils';
jest.mock('~/mr_popover/index', () => jest.fn());
describe('system note component', () => {
let vm;
let props;
+ let mock;
+
+ function createComponent(propsData = {}) {
+ const store = createStore();
+ store.dispatch('setTargetNoteHash', `note_${props.note.id}`);
+
+ vm = mount(IssueSystemNote, {
+ store,
+ propsData,
+ });
+ }
beforeEach(() => {
props = {
@@ -27,28 +41,29 @@ describe('system note component', () => {
},
};
- const store = createStore();
- store.dispatch('setTargetNoteHash', `note_${props.note.id}`);
-
- vm = mount(IssueSystemNote, {
- store,
- propsData: props,
- });
+ mock = new MockAdapter(axios);
});
afterEach(() => {
vm.destroy();
+ mock.restore();
});
it('should render a list item with correct id', () => {
+ createComponent(props);
+
expect(vm.attributes('id')).toEqual(`note_${props.note.id}`);
});
it('should render target class is note is target note', () => {
+ createComponent(props);
+
expect(vm.classes()).toContain('target');
});
it('should render svg icon', () => {
+ createComponent(props);
+
expect(vm.find('.timeline-icon svg').exists()).toBe(true);
});
@@ -56,10 +71,31 @@ describe('system note component', () => {
// we need to strip them because they break layout of commit lists in system notes:
// https://gitlab.com/gitlab-org/gitlab-foss/uploads/b07a10670919254f0220d3ff5c1aa110/jqzI.png
it('removes wrapping paragraph from note HTML', () => {
+ createComponent(props);
+
expect(vm.find('.system-note-message').html()).toContain('<span>closed</span>');
});
it('should initMRPopovers onMount', () => {
+ createComponent(props);
+
expect(initMRPopovers).toHaveBeenCalled();
});
+
+ it('renders outdated code lines', async () => {
+ mock
+ .onGet('/outdated_line_change_path')
+ .reply(200, [
+ { rich_text: 'console.log', type: 'new', line_code: '123', old_line: null, new_line: 1 },
+ ]);
+
+ createComponent({
+ note: { ...props.note, outdated_line_change_path: '/outdated_line_change_path' },
+ });
+
+ await vm.find("[data-testid='outdated-lines-change-btn']").trigger('click');
+ await waitForPromises();
+
+ expect(vm.find("[data-testid='outdated-lines']").exists()).toBe(true);
+ });
});
diff --git a/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js
index 1ed7844b395..7fdacbe83a2 100644
--- a/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js
+++ b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js
@@ -1,6 +1,5 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
-// eslint-disable-next-line import/no-deprecated
-import { getJSONFixture } from 'helpers/fixtures';
+import mockProjects from 'test_fixtures_static/projects.json';
import { trimText } from 'helpers/text_helper';
import ProjectAvatar from '~/vue_shared/components/deprecated_project_avatar/default.vue';
import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
@@ -13,8 +12,7 @@ describe('ProjectListItem component', () => {
let vm;
let options;
- // eslint-disable-next-line import/no-deprecated
- const project = getJSONFixture('static/projects.json')[0];
+ const project = JSON.parse(JSON.stringify(mockProjects))[0];
beforeEach(() => {
options = {
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 1f97d3ff3fa..de5cee846a1 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
@@ -2,8 +2,7 @@ import { GlSearchBoxByType, GlInfiniteScroll } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
import { head } from 'lodash';
import Vue from 'vue';
-// eslint-disable-next-line import/no-deprecated
-import { getJSONFixture } from 'helpers/fixtures';
+import mockProjects from 'test_fixtures_static/projects.json';
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';
@@ -13,8 +12,7 @@ const localVue = createLocalVue();
describe('ProjectSelector component', () => {
let wrapper;
let vm;
- // eslint-disable-next-line import/no-deprecated
- const allProjects = getJSONFixture('static/projects.json');
+ const allProjects = mockProjects;
const searchResults = allProjects.slice(0, 5);
let selected = [];
selected = selected.concat(allProjects.slice(0, 3)).concat(allProjects.slice(5, 8));
diff --git a/spec/frontend/vue_shared/components/registry/title_area_spec.js b/spec/frontend/vue_shared/components/registry/title_area_spec.js
index 75aa3bc7096..b62676b35be 100644
--- a/spec/frontend/vue_shared/components/registry/title_area_spec.js
+++ b/spec/frontend/vue_shared/components/registry/title_area_spec.js
@@ -1,5 +1,6 @@
import { GlAvatar, GlSprintf, GlLink, GlSkeletonLoader } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import component from '~/vue_shared/components/registry/title_area.vue';
describe('title area', () => {
@@ -7,18 +8,18 @@ describe('title area', () => {
const DYNAMIC_SLOT = 'metadata-dynamic-slot';
- const findSubHeaderSlot = () => wrapper.find('[data-testid="sub-header"]');
- const findRightActionsSlot = () => wrapper.find('[data-testid="right-actions"]');
- const findMetadataSlot = (name) => wrapper.find(`[data-testid="${name}"]`);
- const findTitle = () => wrapper.find('[data-testid="title"]');
- const findAvatar = () => wrapper.find(GlAvatar);
- const findInfoMessages = () => wrapper.findAll('[data-testid="info-message"]');
- const findDynamicSlot = () => wrapper.find(`[data-testid="${DYNAMIC_SLOT}`);
+ const findSubHeaderSlot = () => wrapper.findByTestId('sub-header');
+ const findRightActionsSlot = () => wrapper.findByTestId('right-actions');
+ const findMetadataSlot = (name) => wrapper.findByTestId(name);
+ const findTitle = () => wrapper.findByTestId('title');
+ const findAvatar = () => wrapper.findComponent(GlAvatar);
+ const findInfoMessages = () => wrapper.findAllByTestId('info-message');
+ const findDynamicSlot = () => wrapper.findByTestId(DYNAMIC_SLOT);
const findSlotOrderElements = () => wrapper.findAll('[slot-test]');
- const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader);
+ const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const mountComponent = ({ propsData = { title: 'foo' }, slots } = {}) => {
- wrapper = shallowMount(component, {
+ wrapper = shallowMountExtended(component, {
propsData,
stubs: { GlSprintf },
slots: {
@@ -29,6 +30,12 @@ describe('title area', () => {
});
};
+ const generateSlotMocks = (names) =>
+ names.reduce((acc, current) => {
+ acc[current] = `<div data-testid="${current}" />`;
+ return acc;
+ }, {});
+
afterEach(() => {
wrapper.destroy();
wrapper = null;
@@ -40,6 +47,7 @@ describe('title area', () => {
expect(findTitle().text()).toBe('foo');
});
+
it('if slot is present uses slot', () => {
mountComponent({
slots: {
@@ -88,24 +96,21 @@ describe('title area', () => {
${['metadata-foo', 'metadata-bar']}
${['metadata-foo', 'metadata-bar', 'metadata-baz']}
`('$slotNames metadata slots', ({ slotNames }) => {
- const slotMocks = slotNames.reduce((acc, current) => {
- acc[current] = `<div data-testid="${current}" />`;
- return acc;
- }, {});
+ const slots = generateSlotMocks(slotNames);
it('exist when the slot is present', async () => {
- mountComponent({ slots: slotMocks });
+ mountComponent({ slots });
- await wrapper.vm.$nextTick();
+ await nextTick();
slotNames.forEach((name) => {
expect(findMetadataSlot(name).exists()).toBe(true);
});
});
it('is/are hidden when metadata-loading is true', async () => {
- mountComponent({ slots: slotMocks, propsData: { title: 'foo', metadataLoading: true } });
+ mountComponent({ slots, propsData: { title: 'foo', metadataLoading: true } });
- await wrapper.vm.$nextTick();
+ await nextTick();
slotNames.forEach((name) => {
expect(findMetadataSlot(name).exists()).toBe(false);
});
@@ -113,14 +118,20 @@ describe('title area', () => {
});
describe('metadata skeleton loader', () => {
- it('is hidden when metadata loading is false', () => {
- mountComponent();
+ const slots = generateSlotMocks(['metadata-foo']);
+
+ it('is hidden when metadata loading is false', async () => {
+ mountComponent({ slots });
+
+ await nextTick();
expect(findSkeletonLoader().exists()).toBe(false);
});
- it('is shown when metadata loading is true', () => {
- mountComponent({ propsData: { metadataLoading: true } });
+ it('is shown when metadata loading is true', async () => {
+ mountComponent({ propsData: { metadataLoading: true }, slots });
+
+ await nextTick();
expect(findSkeletonLoader().exists()).toBe(true);
});
@@ -143,7 +154,7 @@ describe('title area', () => {
// updating the slots like we do on line 141 does not cause the updated lifecycle-hook to be triggered
wrapper.vm.$forceUpdate();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findDynamicSlot().exists()).toBe(true);
});
@@ -163,7 +174,7 @@ describe('title area', () => {
// updating the slots like we do on line 159 does not cause the updated lifecycle-hook to be triggered
wrapper.vm.$forceUpdate();
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findSlotOrderElements().at(0).attributes('data-testid')).toBe(DYNAMIC_SLOT);
expect(findSlotOrderElements().at(1).attributes('data-testid')).toBe('metadata-foo');
diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
index 32ef2d27ba7..8536ffed573 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
@@ -1,4 +1,4 @@
-import { GlAlert, GlButton, GlLoadingIcon, GlSkeletonLoader } from '@gitlab/ui';
+import { GlAlert, GlModal, GlButton, GlLoadingIcon, GlSkeletonLoader } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
@@ -52,7 +52,7 @@ describe('RunnerInstructionsModal component', () => {
const findBinaryInstructions = () => wrapper.findByTestId('binary-instructions');
const findRegisterCommand = () => wrapper.findByTestId('register-command');
- const createComponent = () => {
+ const createComponent = ({ props, ...options } = {}) => {
const requestHandlers = [
[getRunnerPlatformsQuery, runnerPlatformsHandler],
[getRunnerSetupInstructionsQuery, runnerSetupInstructionsHandler],
@@ -64,9 +64,12 @@ describe('RunnerInstructionsModal component', () => {
shallowMount(RunnerInstructionsModal, {
propsData: {
modalId: 'runner-instructions-modal',
+ registrationToken: 'MY_TOKEN',
+ ...props,
},
localVue,
apolloProvider: fakeApollo,
+ ...options,
}),
);
};
@@ -118,18 +121,30 @@ describe('RunnerInstructionsModal component', () => {
expect(instructions).toBe(installInstructions);
});
- it('register command is shown', () => {
+ it('register command is shown with a replaced token', () => {
const instructions = findRegisterCommand().text();
- expect(instructions).toBe(registerInstructions);
+ expect(instructions).toBe(
+ 'sudo gitlab-runner register --url http://gdk.test:3000/ --registration-token MY_TOKEN',
+ );
+ });
+
+ describe('when a register token is not shown', () => {
+ beforeEach(async () => {
+ createComponent({ props: { registrationToken: undefined } });
+ await nextTick();
+ });
+
+ it('register command is shown without a defined registration token', () => {
+ const instructions = findRegisterCommand().text();
+
+ expect(instructions).toBe(registerInstructions);
+ });
});
});
describe('after a platform and architecture are selected', () => {
- const {
- installInstructions,
- registerInstructions,
- } = mockGraphqlInstructionsWindows.data.runnerSetup;
+ const { installInstructions } = mockGraphqlInstructionsWindows.data.runnerSetup;
beforeEach(async () => {
runnerSetupInstructionsHandler.mockResolvedValue(mockGraphqlInstructionsWindows);
@@ -157,7 +172,9 @@ describe('RunnerInstructionsModal component', () => {
it('register command is shown', () => {
const command = findRegisterCommand().text();
- expect(command).toBe(registerInstructions);
+ expect(command).toBe(
+ './gitlab-runner.exe register --url http://gdk.test:3000/ --registration-token MY_TOKEN',
+ );
});
});
@@ -217,4 +234,36 @@ describe('RunnerInstructionsModal component', () => {
expect(findRegisterCommand().exists()).toBe(false);
});
});
+
+ describe('GlModal API', () => {
+ const getGlModalStub = (methods) => {
+ return {
+ ...GlModal,
+ methods: {
+ ...GlModal.methods,
+ ...methods,
+ },
+ };
+ };
+
+ describe('show()', () => {
+ let mockShow;
+
+ beforeEach(() => {
+ mockShow = jest.fn();
+
+ createComponent({
+ stubs: {
+ GlModal: getGlModalStub({ show: mockShow }),
+ },
+ });
+ });
+
+ it('delegates show()', () => {
+ wrapper.vm.show();
+
+ expect(mockShow).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
});
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
index 165caea2751..a0f46f07d6a 100644
--- 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
@@ -50,6 +50,7 @@ exports[`Settings Block renders the correct markup 1`] = `
class="settings-content"
id="settings_content_3"
role="region"
+ style="display: none;"
tabindex="-1"
>
<div
diff --git a/spec/frontend/vue_shared/components/settings/settings_block_spec.js b/spec/frontend/vue_shared/components/settings/settings_block_spec.js
index 528dfd89690..5e829653c13 100644
--- a/spec/frontend/vue_shared/components/settings/settings_block_spec.js
+++ b/spec/frontend/vue_shared/components/settings/settings_block_spec.js
@@ -1,12 +1,12 @@
import { GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
describe('Settings Block', () => {
let wrapper;
const mountComponent = (propsData) => {
- wrapper = shallowMount(SettingsBlock, {
+ wrapper = shallowMountExtended(SettingsBlock, {
propsData,
slots: {
title: '<div data-testid="title-slot"></div>',
@@ -20,11 +20,13 @@ describe('Settings Block', () => {
wrapper.destroy();
});
- 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 findDefaultSlot = () => wrapper.findByTestId('default-slot');
+ const findTitleSlot = () => wrapper.findByTestId('title-slot');
+ const findDescriptionSlot = () => wrapper.findByTestId('description-slot');
const findExpandButton = () => wrapper.findComponent(GlButton);
- const findSectionTitleButton = () => wrapper.find('[data-testid="section-title-button"]');
+ const findSectionTitleButton = () => wrapper.findByTestId('section-title-button');
+ // we are using a non js class for this finder because this class determine the component structure
+ const findSettingsContent = () => wrapper.find('.settings-content');
const expectExpandedState = ({ expanded = true } = {}) => {
const settingsExpandButton = findExpandButton();
@@ -62,6 +64,26 @@ describe('Settings Block', () => {
expect(findDescriptionSlot().exists()).toBe(true);
});
+ it('content is hidden before first expansion', async () => {
+ // this is a regression test for the bug described here: https://gitlab.com/gitlab-org/gitlab/-/issues/331774
+ mountComponent();
+
+ // content is hidden
+ expect(findDefaultSlot().isVisible()).toBe(false);
+
+ // expand
+ await findSectionTitleButton().trigger('click');
+
+ // content is visible
+ expect(findDefaultSlot().isVisible()).toBe(true);
+
+ // collapse
+ await findSectionTitleButton().trigger('click');
+
+ // content is still visible (and we have a closing animation)
+ expect(findDefaultSlot().isVisible()).toBe(true);
+ });
+
describe('slide animation behaviour', () => {
it('is animated by default', () => {
mountComponent();
@@ -81,6 +103,20 @@ describe('Settings Block', () => {
expect(wrapper.classes('no-animate')).toBe(noAnimatedClass);
},
);
+
+ it('sets the animating class only during the animation', async () => {
+ mountComponent();
+
+ expect(wrapper.classes('animating')).toBe(false);
+
+ await findSectionTitleButton().trigger('click');
+
+ expect(wrapper.classes('animating')).toBe(true);
+
+ await findSettingsContent().trigger('animationend');
+
+ expect(wrapper.classes('animating')).toBe(false);
+ });
});
describe('expanded behaviour', () => {
diff --git a/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js b/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js
index 240d6cb5a34..79e41ed0c9e 100644
--- a/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js
@@ -1,36 +1,68 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import collapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue';
+import { shallowMount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-describe('collapsedCalendarIcon', () => {
- let vm;
- beforeEach(() => {
- const CollapsedCalendarIcon = Vue.extend(collapsedCalendarIcon);
- vm = mountComponent(CollapsedCalendarIcon, {
- containerClass: 'test-class',
- text: 'text',
- showIcon: false,
+import CollapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue';
+
+describe('CollapsedCalendarIcon', () => {
+ let wrapper;
+
+ const defaultProps = {
+ containerClass: 'test-class',
+ text: 'text',
+ tooltipText: 'tooltip text',
+ showIcon: false,
+ };
+
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMount(CollapsedCalendarIcon, {
+ propsData: { ...defaultProps, ...props },
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
});
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
});
- it('should add class to container', () => {
- expect(vm.$el.classList.contains('test-class')).toEqual(true);
+ const findGlIcon = () => wrapper.findComponent(GlIcon);
+ const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip');
+
+ it('adds class to container', () => {
+ expect(wrapper.classes()).toContain(defaultProps.containerClass);
+ });
+
+ it('does not render calendar icon when showIcon is false', () => {
+ expect(findGlIcon().exists()).toBe(false);
+ });
+
+ it('renders calendar icon when showIcon is true', () => {
+ createComponent({
+ props: { showIcon: true },
+ });
+
+ expect(findGlIcon().exists()).toBe(true);
});
- it('should hide calendar icon if showIcon', () => {
- expect(vm.$el.querySelector('[data-testid="calendar-icon"]')).toBeNull();
+ it('renders text', () => {
+ expect(wrapper.text()).toBe(defaultProps.text);
});
- it('should render text', () => {
- expect(vm.$el.querySelector('span').innerText.trim()).toEqual('text');
+ it('renders tooltipText as tooltip', () => {
+ expect(getTooltip().value).toBe(defaultProps.tooltipText);
});
- it('should emit click event when container is clicked', () => {
- const click = jest.fn();
- vm.$on('click', click);
+ it('emits click event when container is clicked', async () => {
+ wrapper.trigger('click');
- vm.$el.click();
+ await wrapper.vm.$nextTick();
- expect(click).toHaveBeenCalled();
+ expect(wrapper.emitted('click')[0]).toBeDefined();
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js b/spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js
index 230442ec547..e72b3bf45c4 100644
--- a/spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js
@@ -1,86 +1,103 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import collapsedGroupedDatePicker from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue';
-
-describe('collapsedGroupedDatePicker', () => {
- let vm;
- beforeEach(() => {
- const CollapsedGroupedDatePicker = Vue.extend(collapsedGroupedDatePicker);
- vm = mountComponent(CollapsedGroupedDatePicker, {
- showToggleSidebar: true,
+import { shallowMount } from '@vue/test-utils';
+
+import CollapsedGroupedDatePicker from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue';
+import CollapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue';
+
+describe('CollapsedGroupedDatePicker', () => {
+ let wrapper;
+
+ const defaultProps = {
+ showToggleSidebar: true,
+ };
+
+ const minDate = new Date('07/17/2016');
+ const maxDate = new Date('07/17/2017');
+
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMount(CollapsedGroupedDatePicker, {
+ propsData: { ...defaultProps, ...props },
});
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
});
- describe('toggleCollapse events', () => {
- beforeEach((done) => {
- jest.spyOn(vm, 'toggleSidebar').mockImplementation(() => {});
- vm.minDate = new Date('07/17/2016');
- Vue.nextTick(done);
- });
+ const findCollapsedCalendarIcon = () => wrapper.findComponent(CollapsedCalendarIcon);
+ const findAllCollapsedCalendarIcons = () => wrapper.findAllComponents(CollapsedCalendarIcon);
+ describe('toggleCollapse events', () => {
it('should emit when collapsed-calendar-icon is clicked', () => {
- vm.$el.querySelector('.sidebar-collapsed-icon').click();
+ createComponent();
- expect(vm.toggleSidebar).toHaveBeenCalled();
+ findCollapsedCalendarIcon().trigger('click');
+
+ expect(wrapper.emitted('toggleCollapse')[0]).toBeDefined();
});
});
describe('minDate and maxDate', () => {
- beforeEach((done) => {
- vm.minDate = new Date('07/17/2016');
- vm.maxDate = new Date('07/17/2017');
- Vue.nextTick(done);
- });
-
it('should render both collapsed-calendar-icon', () => {
- const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
-
- expect(icons.length).toEqual(2);
- expect(icons[0].innerText.trim()).toEqual('Jul 17 2016');
- expect(icons[1].innerText.trim()).toEqual('Jul 17 2017');
+ createComponent({
+ props: {
+ minDate,
+ maxDate,
+ },
+ });
+
+ const icons = findAllCollapsedCalendarIcons();
+
+ expect(icons.length).toBe(2);
+ expect(icons.at(0).text()).toBe('Jul 17 2016');
+ expect(icons.at(1).text()).toBe('Jul 17 2017');
});
});
describe('minDate', () => {
- beforeEach((done) => {
- vm.minDate = new Date('07/17/2016');
- Vue.nextTick(done);
- });
-
it('should render minDate in collapsed-calendar-icon', () => {
- const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
+ createComponent({
+ props: {
+ minDate,
+ },
+ });
+
+ const icons = findAllCollapsedCalendarIcons();
- expect(icons.length).toEqual(1);
- expect(icons[0].innerText.trim()).toEqual('From Jul 17 2016');
+ expect(icons.length).toBe(1);
+ expect(icons.at(0).text()).toBe('From Jul 17 2016');
});
});
describe('maxDate', () => {
- beforeEach((done) => {
- vm.maxDate = new Date('07/17/2017');
- Vue.nextTick(done);
- });
-
it('should render maxDate in collapsed-calendar-icon', () => {
- const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
-
- expect(icons.length).toEqual(1);
- expect(icons[0].innerText.trim()).toEqual('Until Jul 17 2017');
+ createComponent({
+ props: {
+ maxDate,
+ },
+ });
+ const icons = findAllCollapsedCalendarIcons();
+
+ expect(icons.length).toBe(1);
+ expect(icons.at(0).text()).toBe('Until Jul 17 2017');
});
});
describe('no dates', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
it('should render None', () => {
- const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
+ const icons = findAllCollapsedCalendarIcons();
- expect(icons.length).toEqual(1);
- expect(icons[0].innerText.trim()).toEqual('None');
+ expect(icons.length).toBe(1);
+ expect(icons.at(0).text()).toBe('None');
});
it('should have tooltip as `Start and due date`', () => {
- const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
+ const icons = findAllCollapsedCalendarIcons();
- expect(icons[0].title).toBe('Start and due date');
+ expect(icons.at(0).props('tooltipText')).toBe('Start and due date');
});
});
});
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 3221e88192b..263d1e9d947 100644
--- a/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js
@@ -1,3 +1,4 @@
+import { GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import DatePicker from '~/vue_shared/components/pikaday.vue';
import SidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue';
@@ -5,14 +6,8 @@ import SidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue';
describe('SidebarDatePicker', () => {
let wrapper;
- const mountComponent = (propsData = {}, data = {}) => {
- if (wrapper) {
- throw new Error('tried to call mountComponent without d');
- }
+ const createComponent = (propsData = {}, data = {}) => {
wrapper = mount(SidebarDatePicker, {
- stubs: {
- DatePicker: true,
- },
propsData,
data: () => data,
});
@@ -20,87 +15,93 @@ describe('SidebarDatePicker', () => {
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
+ const findDatePicker = () => wrapper.findComponent(DatePicker);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findEditButton = () => wrapper.find('.title .btn-blank');
+ const findRemoveButton = () => wrapper.find('.value-content .btn-blank');
+ const findSidebarToggle = () => wrapper.find('.title .gutter-toggle');
+ const findValueContent = () => wrapper.find('.value-content');
+
it('should emit toggleCollapse when collapsed toggle sidebar is clicked', () => {
- mountComponent();
+ createComponent();
- wrapper.find('.issuable-sidebar-header .gutter-toggle').element.click();
+ wrapper.find('.issuable-sidebar-header .gutter-toggle').trigger('click');
expect(wrapper.emitted('toggleCollapse')).toEqual([[]]);
});
it('should render collapsed-calendar-icon', () => {
- mountComponent();
+ createComponent();
- expect(wrapper.find('.sidebar-collapsed-icon').element).toBeDefined();
+ expect(wrapper.find('.sidebar-collapsed-icon').exists()).toBe(true);
});
it('should render value when not editing', () => {
- mountComponent();
+ createComponent();
- expect(wrapper.find('.value-content').element).toBeDefined();
+ expect(findValueContent().exists()).toBe(true);
});
it('should render None if there is no selectedDate', () => {
- mountComponent();
+ createComponent();
- expect(wrapper.find('.value-content span').text().trim()).toEqual('None');
+ expect(findValueContent().text()).toBe('None');
});
it('should render date-picker when editing', () => {
- mountComponent({}, { editing: true });
+ createComponent({}, { editing: true });
- expect(wrapper.find(DatePicker).element).toBeDefined();
+ expect(findDatePicker().exists()).toBe(true);
});
it('should render label', () => {
const label = 'label';
- mountComponent({ label });
- expect(wrapper.find('.title').text().trim()).toEqual(label);
+ createComponent({ label });
+ expect(wrapper.find('.title').text()).toBe(label);
});
it('should render loading-icon when isLoading', () => {
- mountComponent({ isLoading: true });
- expect(wrapper.find('.gl-spinner').element).toBeDefined();
+ createComponent({ isLoading: true });
+ expect(findLoadingIcon().exists()).toBe(true);
});
describe('editable', () => {
beforeEach(() => {
- mountComponent({ editable: true });
+ createComponent({ editable: true });
});
it('should render edit button', () => {
- expect(wrapper.find('.title .btn-blank').text().trim()).toEqual('Edit');
+ expect(findEditButton().text()).toBe('Edit');
});
it('should enable editing when edit button is clicked', async () => {
- wrapper.find('.title .btn-blank').element.click();
+ findEditButton().trigger('click');
await wrapper.vm.$nextTick();
- expect(wrapper.vm.editing).toEqual(true);
+ expect(wrapper.vm.editing).toBe(true);
});
});
it('should render date if selectedDate', () => {
- mountComponent({ selectedDate: new Date('07/07/2017') });
+ createComponent({ selectedDate: new Date('07/07/2017') });
- expect(wrapper.find('.value-content strong').text().trim()).toEqual('Jul 7, 2017');
+ expect(wrapper.find('.value-content strong').text()).toBe('Jul 7, 2017');
});
describe('selectedDate and editable', () => {
beforeEach(() => {
- mountComponent({ selectedDate: new Date('07/07/2017'), editable: true });
+ createComponent({ selectedDate: new Date('07/07/2017'), editable: true });
});
it('should render remove button if selectedDate and editable', () => {
- expect(wrapper.find('.value-content .btn-blank').text().trim()).toEqual('remove');
+ expect(findRemoveButton().text()).toBe('remove');
});
it('should emit saveDate with null when remove button is clicked', () => {
- wrapper.find('.value-content .btn-blank').element.click();
+ findRemoveButton().trigger('click');
expect(wrapper.emitted('saveDate')).toEqual([[null]]);
});
@@ -108,15 +109,15 @@ describe('SidebarDatePicker', () => {
describe('showToggleSidebar', () => {
beforeEach(() => {
- mountComponent({ showToggleSidebar: true });
+ createComponent({ showToggleSidebar: true });
});
it('should render toggle-sidebar when showToggleSidebar', () => {
- expect(wrapper.find('.title .gutter-toggle').element).toBeDefined();
+ expect(findSidebarToggle().exists()).toBe(true);
});
it('should emit toggleCollapse when toggle sidebar is clicked', () => {
- wrapper.find('.title .gutter-toggle').element.click();
+ findSidebarToggle().trigger('click');
expect(wrapper.emitted('toggleCollapse')).toEqual([[]]);
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js
index 8c1693e8dcc..a7f9391cb5f 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js
@@ -1,95 +1,74 @@
-import Vue from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import DropdownValueCollapsedComponent from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import dropdownValueCollapsedComponent from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue';
+import { mockCollapsedLabels as mockLabels, mockRegularLabel } from './mock_data';
-import { mockCollapsedLabels as mockLabels } from './mock_data';
-
-const createComponent = (labels = mockLabels) => {
- const Component = Vue.extend(dropdownValueCollapsedComponent);
+describe('DropdownValueCollapsedComponent', () => {
+ let wrapper;
- return mountComponent(Component, {
- labels,
- });
-};
+ const defaultProps = {
+ labels: [],
+ };
-describe('DropdownValueCollapsedComponent', () => {
- let vm;
+ const mockManyLabels = [...mockLabels, ...mockLabels, ...mockLabels];
- beforeEach(() => {
- vm = createComponent();
- });
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMount(DropdownValueCollapsedComponent, {
+ propsData: { ...defaultProps, ...props },
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ });
+ };
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
- describe('computed', () => {
- describe('labelsList', () => {
- it('returns default text when `labels` prop is empty array', () => {
- const vmEmptyLabels = createComponent([]);
+ const findGlIcon = () => wrapper.findComponent(GlIcon);
+ const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip');
- expect(vmEmptyLabels.labelsList).toBe('Labels');
- vmEmptyLabels.$destroy();
- });
-
- it('returns labels names separated by coma when `labels` prop has more than one item', () => {
- const labels = mockLabels.concat(mockLabels);
- const vmMoreLabels = createComponent(labels);
+ describe('template', () => {
+ it('renders tags icon element', () => {
+ createComponent();
- const expectedText = labels.map((label) => label.title).join(', ');
+ expect(findGlIcon().exists()).toBe(true);
+ });
- expect(vmMoreLabels.labelsList).toBe(expectedText);
- vmMoreLabels.$destroy();
- });
+ it('emits onValueClick event on click', async () => {
+ createComponent();
- it('returns labels names separated by coma with remaining labels count and `and more` phrase when `labels` prop has more than five items', () => {
- const mockMoreLabels = Object.assign([], mockLabels);
- for (let i = 0; i < 6; i += 1) {
- mockMoreLabels.unshift(mockLabels[0]);
- }
+ wrapper.trigger('click');
- const vmMoreLabels = createComponent(mockMoreLabels);
+ await wrapper.vm.$nextTick();
- const expectedText = `${mockMoreLabels
- .slice(0, 5)
- .map((label) => label.title)
- .join(', ')}, and ${mockMoreLabels.length - 5} more`;
+ expect(wrapper.emitted('onValueClick')[0]).toBeDefined();
+ });
- expect(vmMoreLabels.labelsList).toBe(expectedText);
- vmMoreLabels.$destroy();
+ describe.each`
+ scenario | labels | expectedResult | expectedText
+ ${'`labels` is empty'} | ${[]} | ${'default text'} | ${'Labels'}
+ ${'`labels` has 1 item'} | ${[mockRegularLabel]} | ${'label name'} | ${'Foo Label'}
+ ${'`labels` has 2 items'} | ${mockLabels} | ${'comma separated label names'} | ${'Foo Label, Foo::Bar'}
+ ${'`labels` has more than 5 items'} | ${mockManyLabels} | ${'comma separated label names with "and more" phrase'} | ${'Foo Label, Foo::Bar, Foo Label, Foo::Bar, Foo Label, and 1 more'}
+ `('when $scenario', ({ labels, expectedResult, expectedText }) => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ labels,
+ },
+ });
});
- it('returns first label name when `labels` prop has only one item present', () => {
- const text = mockLabels.map((label) => label.title).join(', ');
-
- expect(vm.labelsList).toBe(text);
+ it('renders labels count', () => {
+ expect(wrapper.text()).toBe(`${labels.length}`);
});
- });
- });
-
- describe('methods', () => {
- describe('handleClick', () => {
- it('emits onValueClick event on component', () => {
- jest.spyOn(vm, '$emit').mockImplementation(() => {});
- vm.handleClick();
- expect(vm.$emit).toHaveBeenCalledWith('onValueClick');
+ it(`renders "${expectedResult}" as tooltip`, () => {
+ expect(getTooltip().value).toBe(expectedText);
});
});
});
-
- describe('template', () => {
- it('renders component container element with tooltip`', () => {
- expect(vm.$el.title).toBe(vm.labelsList);
- });
-
- it('renders tags icon element', () => {
- expect(vm.$el.querySelector('[data-testid="labels-icon"]')).not.toBeNull();
- });
-
- it('renders labels count', () => {
- expect(vm.$el.querySelector('span').innerText.trim()).toBe(`${vm.labels.length}`);
- });
- });
});
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 d9b7cd5afa2..a60e6f52862 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,3 +1,4 @@
+import { cloneDeep } from 'lodash';
import * as types from '~/vue_shared/components/sidebar/labels_select_vue/store/mutation_types';
import mutations from '~/vue_shared/components/sidebar/labels_select_vue/store/mutations';
@@ -153,47 +154,40 @@ describe('LabelsSelect Mutations', () => {
});
describe(`${types.UPDATE_SELECTED_LABELS}`, () => {
- let labels;
-
- beforeEach(() => {
- labels = [
- { id: 1, title: 'scoped' },
- { id: 2, title: 'scoped::one', set: false },
- { id: 3, title: 'scoped::test', set: true },
- { id: 4, title: '' },
- ];
- });
-
- it('updates `state.labels` to include `touched` and `set` props based on provided `labels` param', () => {
- const updatedLabelIds = [2];
- const state = {
- labels,
- };
- mutations[types.UPDATE_SELECTED_LABELS](state, { labels: [{ id: 2 }] });
-
- state.labels.forEach((label) => {
- if (updatedLabelIds.includes(label.id)) {
- expect(label.touched).toBe(true);
- expect(label.set).toBe(true);
- }
+ const labels = [
+ { id: 1, title: 'scoped' },
+ { id: 2, title: 'scoped::label::one', set: false },
+ { id: 3, title: 'scoped::label::two', set: false },
+ { id: 4, title: 'scoped::label::three', set: true },
+ { id: 5, title: 'scoped::one', set: false },
+ { id: 6, title: 'scoped::two', set: false },
+ { id: 7, title: 'scoped::three', set: true },
+ { id: 8, title: '' },
+ ];
+
+ it.each`
+ label | labelGroupIds
+ ${labels[0]} | ${[]}
+ ${labels[1]} | ${[labels[2], labels[3]]}
+ ${labels[2]} | ${[labels[1], labels[3]]}
+ ${labels[3]} | ${[labels[1], labels[2]]}
+ ${labels[4]} | ${[labels[5], labels[6]]}
+ ${labels[5]} | ${[labels[4], labels[6]]}
+ ${labels[6]} | ${[labels[4], labels[5]]}
+ ${labels[7]} | ${[]}
+ `('updates `touched` and `set` props for $label.title', ({ label, labelGroupIds }) => {
+ const state = { labels: cloneDeep(labels) };
+
+ mutations[types.UPDATE_SELECTED_LABELS](state, { labels: [{ id: label.id }] });
+
+ expect(state.labels[label.id - 1]).toMatchObject({
+ touched: true,
+ set: !labels[label.id - 1].set,
});
- });
- describe('when label is scoped', () => {
- it('unsets the currently selected scoped label and sets the current label', () => {
- const state = {
- labels,
- };
- mutations[types.UPDATE_SELECTED_LABELS](state, {
- labels: [{ id: 2, title: 'scoped::one' }],
- });
-
- expect(state.labels).toEqual([
- { id: 1, title: 'scoped' },
- { id: 2, title: 'scoped::one', set: true, touched: true },
- { id: 3, title: 'scoped::test', set: false },
- { id: 4, title: '' },
- ]);
+ labelGroupIds.forEach((l) => {
+ expect(state.labels[l.id - 1].touched).toBeFalsy();
+ expect(state.labels[l.id - 1].set).toBe(false);
});
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
index 8931584e12c..bf873f9162b 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view_spec.js
@@ -5,8 +5,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
-import { IssuableType } from '~/issue_show/constants';
-import { labelsQueries } from '~/sidebar/constants';
+import { workspaceLabelsQueries } from '~/sidebar/constants';
import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue';
import createLabelMutation from '~/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql';
import {
@@ -50,11 +49,12 @@ describe('DropdownContentsCreateView', () => {
const createComponent = ({
mutationHandler = createLabelSuccessHandler,
- issuableType = IssuableType.Issue,
+ labelCreateType = 'project',
+ workspaceType = 'project',
} = {}) => {
const mockApollo = createMockApollo([[createLabelMutation, mutationHandler]]);
mockApollo.clients.defaultClient.cache.writeQuery({
- query: labelsQueries[issuableType].workspaceQuery,
+ query: workspaceLabelsQueries[workspaceType].query,
data: workspaceLabelsQueryResponse.data,
variables: {
fullPath: '',
@@ -66,8 +66,10 @@ describe('DropdownContentsCreateView', () => {
localVue,
apolloProvider: mockApollo,
propsData: {
- issuableType,
fullPath: '',
+ attrWorkspacePath: '',
+ labelCreateType,
+ workspaceType,
},
});
};
@@ -128,9 +130,11 @@ describe('DropdownContentsCreateView', () => {
it('emits a `hideCreateView` event on Cancel button click', () => {
createComponent();
- findCancelButton().vm.$emit('click');
+ const event = { stopPropagation: jest.fn() };
+ findCancelButton().vm.$emit('click', event);
expect(wrapper.emitted('hideCreateView')).toHaveLength(1);
+ expect(event.stopPropagation).toHaveBeenCalled();
});
describe('when label title and selected color are set', () => {
@@ -174,7 +178,7 @@ describe('DropdownContentsCreateView', () => {
});
it('calls a mutation with `groupPath` variable on the epic', () => {
- createComponent({ issuableType: IssuableType.Epic });
+ createComponent({ labelCreateType: 'group', workspaceType: 'group' });
fillLabelAttributes();
findCreateButton().vm.$emit('click');
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
index fac3331a2b8..2980409fdce 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js
@@ -10,7 +10,6 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
-import { IssuableType } from '~/issue_show/constants';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue';
@@ -43,6 +42,7 @@ describe('DropdownContentsLabelsView', () => {
initialState = mockConfig,
queryHandler = successfulQueryHandler,
injected = {},
+ searchKey = '',
} = {}) => {
const mockApollo = createMockApollo([[projectLabelsQuery, queryHandler]]);
@@ -56,7 +56,9 @@ describe('DropdownContentsLabelsView', () => {
propsData: {
...initialState,
localSelectedLabels,
- issuableType: IssuableType.Issue,
+ searchKey,
+ labelCreateType: 'project',
+ workspaceType: 'project',
},
stubs: {
GlSearchBoxByType,
@@ -68,7 +70,6 @@ describe('DropdownContentsLabelsView', () => {
wrapper.destroy();
});
- const findSearchInput = () => wrapper.findComponent(GlSearchBoxByType);
const findLabels = () => wrapper.findAllComponents(LabelItem);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findObserver = () => wrapper.findComponent(GlIntersectionObserver);
@@ -81,12 +82,6 @@ describe('DropdownContentsLabelsView', () => {
}
describe('when loading labels', () => {
- it('renders disabled search input field', async () => {
- createComponent();
- await makeObserverAppear();
- expect(findSearchInput().props('disabled')).toBe(true);
- });
-
it('renders loading icon', async () => {
createComponent();
await makeObserverAppear();
@@ -107,10 +102,6 @@ describe('DropdownContentsLabelsView', () => {
await waitForPromises();
});
- it('renders enabled search input field', async () => {
- expect(findSearchInput().props('disabled')).toBe(false);
- });
-
it('does not render loading icon', async () => {
expect(findLoadingIcon().exists()).toBe(false);
});
@@ -132,9 +123,9 @@ describe('DropdownContentsLabelsView', () => {
},
},
}),
+ searchKey: '123',
});
await makeObserverAppear();
- findSearchInput().vm.$emit('input', '123');
await waitForPromises();
await nextTick();
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js
index 36704ac5ef3..8bcef347c96 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_spec.js
@@ -4,6 +4,8 @@ import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_w
import DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue';
import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue';
import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue';
+import DropdownHeader from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue';
+import DropdownFooter from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue';
import { mockLabels } from './mock_data';
@@ -26,7 +28,7 @@ const GlDropdownStub = {
describe('DropdownContent', () => {
let wrapper;
- const createComponent = ({ props = {}, injected = {}, data = {} } = {}) => {
+ const createComponent = ({ props = {}, data = {} } = {}) => {
wrapper = shallowMount(DropdownContents, {
propsData: {
labelsCreateTitle: 'test',
@@ -37,8 +39,10 @@ describe('DropdownContent', () => {
footerManageLabelTitle: 'manage',
dropdownButtonText: 'Labels',
variant: 'sidebar',
- issuableType: 'issue',
fullPath: 'test',
+ workspaceType: 'project',
+ labelCreateType: 'project',
+ attrWorkspacePath: 'path',
...props,
},
data() {
@@ -46,11 +50,6 @@ describe('DropdownContent', () => {
...data,
};
},
- provide: {
- allowLabelCreate: true,
- labelsManagePath: 'foo/bar',
- ...injected,
- },
stubs: {
GlDropdown: GlDropdownStub,
},
@@ -63,13 +62,10 @@ describe('DropdownContent', () => {
const findCreateView = () => wrapper.findComponent(DropdownContentsCreateView);
const findLabelsView = () => wrapper.findComponent(DropdownContentsLabelsView);
+ const findDropdownHeader = () => wrapper.findComponent(DropdownHeader);
+ const findDropdownFooter = () => wrapper.findComponent(DropdownFooter);
const findDropdown = () => wrapper.findComponent(GlDropdownStub);
- const findDropdownFooter = () => wrapper.find('[data-testid="dropdown-footer"]');
- const findDropdownHeader = () => wrapper.find('[data-testid="dropdown-header"]');
- const findCreateLabelButton = () => wrapper.find('[data-testid="create-label-button"]');
- const findGoBackButton = () => wrapper.find('[data-testid="go-back-button"]');
-
it('calls dropdown `show` method on `isVisible` prop change', async () => {
createComponent();
await wrapper.setProps({
@@ -136,6 +132,16 @@ describe('DropdownContent', () => {
expect(findDropdownHeader().exists()).toBe(true);
});
+ it('sets searchKey for labels view on input event from header', async () => {
+ createComponent();
+
+ expect(wrapper.vm.searchKey).toEqual('');
+ findDropdownHeader().vm.$emit('input', '123');
+ await nextTick();
+
+ expect(findLabelsView().props('searchKey')).toEqual('123');
+ });
+
describe('Create view', () => {
beforeEach(() => {
createComponent({ data: { showDropdownContentsCreateView: true } });
@@ -149,16 +155,8 @@ describe('DropdownContent', () => {
expect(findDropdownFooter().exists()).toBe(false);
});
- it('does not render create label button', () => {
- expect(findCreateLabelButton().exists()).toBe(false);
- });
-
- it('renders go back button', () => {
- expect(findGoBackButton().exists()).toBe(true);
- });
-
- it('changes the view to Labels view on back button click', async () => {
- findGoBackButton().vm.$emit('click', new MouseEvent('click'));
+ it('changes the view to Labels view on `toggleDropdownContentsCreateView` event', async () => {
+ findDropdownHeader().vm.$emit('toggleDropdownContentsCreateView');
await nextTick();
expect(findCreateView().exists()).toBe(false);
@@ -198,32 +196,5 @@ describe('DropdownContent', () => {
expect(findDropdownFooter().exists()).toBe(true);
});
-
- it('does not render go back button', () => {
- expect(findGoBackButton().exists()).toBe(false);
- });
-
- it('does not render create label button if `allowLabelCreate` is false', () => {
- createComponent({ injected: { allowLabelCreate: false } });
-
- expect(findCreateLabelButton().exists()).toBe(false);
- });
-
- describe('when `allowLabelCreate` is true', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('renders create label button', () => {
- expect(findCreateLabelButton().exists()).toBe(true);
- });
-
- it('changes the view to Create on create label button click', async () => {
- findCreateLabelButton().trigger('click');
-
- await nextTick();
- expect(findLabelsView().exists()).toBe(false);
- });
- });
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_footer_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_footer_spec.js
new file mode 100644
index 00000000000..0508a059195
--- /dev/null
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_footer_spec.js
@@ -0,0 +1,57 @@
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import DropdownFooter from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue';
+
+describe('DropdownFooter', () => {
+ let wrapper;
+
+ const createComponent = ({ props = {}, injected = {} } = {}) => {
+ wrapper = shallowMount(DropdownFooter, {
+ propsData: {
+ footerCreateLabelTitle: 'create',
+ footerManageLabelTitle: 'manage',
+ ...props,
+ },
+ provide: {
+ allowLabelCreate: true,
+ labelsManagePath: 'foo/bar',
+ ...injected,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findCreateLabelButton = () => wrapper.find('[data-testid="create-label-button"]');
+
+ describe('Labels view', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('does not render create label button if `allowLabelCreate` is false', () => {
+ createComponent({ injected: { allowLabelCreate: false } });
+
+ expect(findCreateLabelButton().exists()).toBe(false);
+ });
+
+ describe('when `allowLabelCreate` is true', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders create label button', () => {
+ expect(findCreateLabelButton().exists()).toBe(true);
+ });
+
+ it('emits `toggleDropdownContentsCreateView` event on create label button click', async () => {
+ findCreateLabelButton().trigger('click');
+
+ await nextTick();
+ expect(wrapper.emitted('toggleDropdownContentsCreateView')).toEqual([[]]);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_header_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_header_spec.js
new file mode 100644
index 00000000000..592559ef305
--- /dev/null
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_header_spec.js
@@ -0,0 +1,75 @@
+import { GlSearchBoxByType } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import DropdownHeader from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue';
+
+describe('DropdownHeader', () => {
+ let wrapper;
+
+ const createComponent = ({
+ showDropdownContentsCreateView = false,
+ labelsFetchInProgress = false,
+ } = {}) => {
+ wrapper = extendedWrapper(
+ shallowMount(DropdownHeader, {
+ propsData: {
+ showDropdownContentsCreateView,
+ labelsFetchInProgress,
+ labelsCreateTitle: 'Create label',
+ labelsListTitle: 'Select label',
+ searchKey: '',
+ },
+ stubs: {
+ GlSearchBoxByType,
+ },
+ }),
+ );
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findSearchInput = () => wrapper.findComponent(GlSearchBoxByType);
+ const findGoBackButton = () => wrapper.findByTestId('go-back-button');
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('Create view', () => {
+ beforeEach(() => {
+ createComponent({ showDropdownContentsCreateView: true });
+ });
+
+ it('renders go back button', () => {
+ expect(findGoBackButton().exists()).toBe(true);
+ });
+
+ it('does not render search input field', async () => {
+ expect(findSearchInput().exists()).toBe(false);
+ });
+ });
+
+ describe('Labels view', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('does not render go back button', () => {
+ expect(findGoBackButton().exists()).toBe(false);
+ });
+
+ it.each`
+ labelsFetchInProgress | disabled
+ ${true} | ${true}
+ ${false} | ${false}
+ `(
+ 'when labelsFetchInProgress is $labelsFetchInProgress, renders search input with disabled prop to $disabled',
+ ({ labelsFetchInProgress, disabled }) => {
+ createComponent({ labelsFetchInProgress });
+ expect(findSearchInput().props('disabled')).toBe(disabled);
+ },
+ );
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
index b5441d711a5..d4203528874 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
@@ -41,6 +41,8 @@ describe('LabelsSelectRoot', () => {
propsData: {
...config,
issuableType: IssuableType.Issue,
+ labelCreateType: 'project',
+ workspaceType: 'project',
},
stubs: {
SidebarEditableItem,
@@ -121,11 +123,11 @@ describe('LabelsSelectRoot', () => {
});
});
- it('emits `updateSelectedLabels` event on dropdown contents `setLabels` event', async () => {
+ it('emits `updateSelectedLabels` event on dropdown contents `setLabels` event if iid is not set', async () => {
const label = { id: 'gid://gitlab/ProjectLabel/1' };
- createComponent();
+ createComponent({ config: { ...mockConfig, iid: undefined } });
findDropdownContents().vm.$emit('setLabels', [label]);
- expect(wrapper.emitted('updateSelectedLabels')).toEqual([[[label]]]);
+ expect(wrapper.emitted('updateSelectedLabels')).toEqual([[{ labels: [label] }]]);
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
index 23a457848d9..5c5bf5f2187 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js
@@ -40,12 +40,12 @@ export const mockConfig = {
labelsListTitle: 'Assign labels',
labelsCreateTitle: 'Create label',
variant: 'sidebar',
- selectedLabels: [mockRegularLabel, mockScopedLabel],
labelsSelectInProgress: false,
labelsFilterBasePath: '/gitlab-org/my-project/issues',
labelsFilterParam: 'label_name',
footerCreateLabelTitle: 'create',
footerManageLabelTitle: 'manage',
+ attrWorkspacePath: 'test',
};
export const mockSuggestedColors = {
@@ -80,6 +80,7 @@ export const createLabelSuccessfulResponse = {
color: '#dc143c',
description: null,
title: 'ewrwrwer',
+ textColor: '#000000',
__typename: 'Label',
},
errors: [],
@@ -91,6 +92,7 @@ export const createLabelSuccessfulResponse = {
export const workspaceLabelsQueryResponse = {
data: {
workspace: {
+ id: 'gid://gitlab/Project/126',
labels: {
nodes: [
{
@@ -98,12 +100,14 @@ export const workspaceLabelsQueryResponse = {
description: null,
id: 'gid://gitlab/ProjectLabel/1',
title: 'Label1',
+ textColor: '#000000',
},
{
color: '#2f7b2e',
description: null,
id: 'gid://gitlab/ProjectLabel/2',
title: 'Label2',
+ textColor: '#000000',
},
],
},
@@ -123,6 +127,7 @@ export const issuableLabelsQueryResponse = {
description: null,
id: 'gid://gitlab/ProjectLabel/1',
title: 'Label1',
+ textColor: '#000000',
},
],
},
diff --git a/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js b/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js
index f1c3e8a1ddc..a6c9bda1aa2 100644
--- a/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js
@@ -1,31 +1,45 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import toggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue';
-
-describe('toggleSidebar', () => {
- let vm;
- beforeEach(() => {
- const ToggleSidebar = Vue.extend(toggleSidebar);
- vm = mountComponent(ToggleSidebar, {
- collapsed: true,
+import { GlButton } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
+
+import ToggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue';
+
+describe('ToggleSidebar', () => {
+ let wrapper;
+
+ const defaultProps = {
+ collapsed: true,
+ };
+
+ const createComponent = ({ mountFn = shallowMount, props = {} } = {}) => {
+ wrapper = mountFn(ToggleSidebar, {
+ propsData: { ...defaultProps, ...props },
});
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
});
+ const findGlButton = () => wrapper.findComponent(GlButton);
+
it('should render the "chevron-double-lg-left" icon when collapsed', () => {
- expect(vm.$el.querySelector('[data-testid="chevron-double-lg-left-icon"]')).not.toBeNull();
+ createComponent();
+
+ expect(findGlButton().props('icon')).toBe('chevron-double-lg-left');
});
it('should render the "chevron-double-lg-right" icon when expanded', async () => {
- vm.collapsed = false;
- await Vue.nextTick();
- expect(vm.$el.querySelector('[data-testid="chevron-double-lg-right-icon"]')).not.toBeNull();
+ createComponent({ props: { collapsed: false } });
+
+ expect(findGlButton().props('icon')).toBe('chevron-double-lg-right');
});
- it('should emit toggle event when button clicked', () => {
- const toggle = jest.fn();
- vm.$on('toggle', toggle);
- vm.$el.click();
+ it('should emit toggle event when button clicked', async () => {
+ createComponent({ mountFn: mount });
+
+ findGlButton().trigger('click');
+ await wrapper.vm.$nextTick();
- expect(toggle).toHaveBeenCalled();
+ expect(wrapper.emitted('toggle')[0]).toBeDefined();
});
});
diff --git a/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js b/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js
index a92f058f311..78abb89e7b8 100644
--- a/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js
+++ b/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js
@@ -82,7 +82,7 @@ describe('User deletion obstacles list', () => {
createComponent({ obstacles: [{ type, name, url, projectName, projectUrl }] });
const msg = findObstacles().text();
- expect(msg).toContain(`in Project ${projectName}`);
+ expect(msg).toContain(`in project ${projectName}`);
expect(findLinks().at(1).attributes('href')).toBe(projectUrl);
});
},
diff --git a/spec/frontend/whats_new/utils/notification_spec.js b/spec/frontend/whats_new/utils/notification_spec.js
index c361f934e59..ef61462a3c5 100644
--- a/spec/frontend/whats_new/utils/notification_spec.js
+++ b/spec/frontend/whats_new/utils/notification_spec.js
@@ -29,7 +29,7 @@ describe('~/whats_new/utils/notification', () => {
subject();
- expect(findNotificationCountEl()).toExist();
+ expect(findNotificationCountEl()).not.toBe(null);
expect(notificationEl.classList).toContain('with-notifications');
});
@@ -38,11 +38,11 @@ describe('~/whats_new/utils/notification', () => {
notificationEl.classList.add('with-notifications');
localStorage.setItem('display-whats-new-notification', 'version-digest');
- expect(findNotificationCountEl()).toExist();
+ expect(findNotificationCountEl()).not.toBe(null);
subject();
- expect(findNotificationCountEl()).not.toExist();
+ expect(findNotificationCountEl()).toBe(null);
expect(notificationEl.classList).not.toContain('with-notifications');
});
});
diff --git a/spec/frontend/work_items/components/app_spec.js b/spec/frontend/work_items/components/app_spec.js
new file mode 100644
index 00000000000..95034085493
--- /dev/null
+++ b/spec/frontend/work_items/components/app_spec.js
@@ -0,0 +1,24 @@
+import { shallowMount } from '@vue/test-utils';
+import App from '~/work_items/components/app.vue';
+
+describe('Work Items Application', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(App, {
+ stubs: {
+ 'router-view': true,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders a component', () => {
+ createComponent();
+
+ expect(wrapper.exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
new file mode 100644
index 00000000000..efb4aa2feb2
--- /dev/null
+++ b/spec/frontend/work_items/mock_data.js
@@ -0,0 +1,17 @@
+export const workItemQueryResponse = {
+ workItem: {
+ __typename: 'WorkItem',
+ id: '1',
+ type: 'FEATURE',
+ widgets: {
+ __typename: 'WorkItemWidgetConnection',
+ nodes: [
+ {
+ __typename: 'TitleWidget',
+ type: 'TITLE',
+ contentText: 'Test',
+ },
+ ],
+ },
+ },
+};
diff --git a/spec/frontend/work_items/pages/work_item_root_spec.js b/spec/frontend/work_items/pages/work_item_root_spec.js
new file mode 100644
index 00000000000..64d02baed36
--- /dev/null
+++ b/spec/frontend/work_items/pages/work_item_root_spec.js
@@ -0,0 +1,70 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
+import WorkItemsRoot from '~/work_items/pages/work_item_root.vue';
+import { workItemQueryResponse } from '../mock_data';
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+const WORK_ITEM_ID = '1';
+
+describe('Work items root component', () => {
+ let wrapper;
+ let fakeApollo;
+
+ const findTitle = () => wrapper.find('[data-testid="title"]');
+
+ const createComponent = ({ queryResponse = workItemQueryResponse } = {}) => {
+ fakeApollo = createMockApollo();
+ fakeApollo.clients.defaultClient.cache.writeQuery({
+ query: workItemQuery,
+ variables: {
+ id: WORK_ITEM_ID,
+ },
+ data: queryResponse,
+ });
+
+ wrapper = shallowMount(WorkItemsRoot, {
+ propsData: {
+ id: WORK_ITEM_ID,
+ },
+ localVue,
+ apolloProvider: fakeApollo,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ fakeApollo = null;
+ });
+
+ it('renders the title if title is in the widgets list', () => {
+ createComponent();
+
+ expect(findTitle().exists()).toBe(true);
+ expect(findTitle().text()).toBe('Test');
+ });
+
+ it('does not render the title if title is not in the widgets list', () => {
+ const queryResponse = {
+ workItem: {
+ ...workItemQueryResponse.workItem,
+ widgets: {
+ __typename: 'WorkItemWidgetConnection',
+ nodes: [
+ {
+ __typename: 'SomeOtherWidget',
+ type: 'OTHER',
+ contentText: 'Test',
+ },
+ ],
+ },
+ },
+ };
+ createComponent({ queryResponse });
+
+ expect(findTitle().exists()).toBe(false);
+ });
+});
diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js
new file mode 100644
index 00000000000..0a57eab753f
--- /dev/null
+++ b/spec/frontend/work_items/router_spec.js
@@ -0,0 +1,30 @@
+import { mount } from '@vue/test-utils';
+import App from '~/work_items/components/app.vue';
+import WorkItemsRoot from '~/work_items/pages/work_item_root.vue';
+import { createRouter } from '~/work_items/router';
+
+describe('Work items router', () => {
+ let wrapper;
+
+ const createComponent = async (routeArg) => {
+ const router = createRouter('/work_item');
+ if (routeArg !== undefined) {
+ await router.push(routeArg);
+ }
+
+ wrapper = mount(App, {
+ router,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ window.location.hash = '';
+ });
+
+ it('renders work item on `/1` route', async () => {
+ await createComponent('/1');
+
+ expect(wrapper.find(WorkItemsRoot).exists()).toBe(true);
+ });
+});
diff --git a/spec/graphql/mutations/customer_relations/contacts/create_spec.rb b/spec/graphql/mutations/customer_relations/contacts/create_spec.rb
index 21a1aa2741a..0f05504d4f2 100644
--- a/spec/graphql/mutations/customer_relations/contacts/create_spec.rb
+++ b/spec/graphql/mutations/customer_relations/contacts/create_spec.rb
@@ -29,7 +29,7 @@ RSpec.describe Mutations::CustomerRelations::Contacts::Create do
it 'raises an error' do
expect { resolve_mutation }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
- .with_message("The resource that you are attempting to access does not exist or you don't have permission to perform this action")
+ .with_message(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
end
end
@@ -45,7 +45,7 @@ RSpec.describe Mutations::CustomerRelations::Contacts::Create do
it 'raises an error' do
expect { resolve_mutation }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
- .with_message('Feature disabled')
+ .with_message("The resource that you are attempting to access does not exist or you don't have permission to perform this action")
end
end
@@ -97,5 +97,5 @@ RSpec.describe Mutations::CustomerRelations::Contacts::Create do
end
end
- specify { expect(described_class).to require_graphql_authorizations(:admin_contact) }
+ specify { expect(described_class).to require_graphql_authorizations(:admin_crm_contact) }
end
diff --git a/spec/graphql/mutations/customer_relations/contacts/update_spec.rb b/spec/graphql/mutations/customer_relations/contacts/update_spec.rb
index 93bc6f53cf9..4f59de194fd 100644
--- a/spec/graphql/mutations/customer_relations/contacts/update_spec.rb
+++ b/spec/graphql/mutations/customer_relations/contacts/update_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Mutations::CustomerRelations::Contacts::Update do
let(:last_name) { 'Smith' }
let(:email) { 'ls@gitlab.com' }
let(:description) { 'VIP' }
- let(:does_not_exist_or_no_permission) { "The resource that you are attempting to access does not exist or you don't have permission to perform this action" }
+ let(:does_not_exist_or_no_permission) { Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR }
let(:contact) { create(:contact, group: group) }
let(:attributes) do
{
@@ -65,11 +65,11 @@ RSpec.describe Mutations::CustomerRelations::Contacts::Update do
it 'raises an error' do
expect { resolve_mutation }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
- .with_message('Feature disabled')
+ .with_message("The resource that you are attempting to access does not exist or you don't have permission to perform this action")
end
end
end
end
- specify { expect(described_class).to require_graphql_authorizations(:admin_contact) }
+ specify { expect(described_class).to require_graphql_authorizations(:admin_crm_contact) }
end
diff --git a/spec/graphql/mutations/customer_relations/organizations/create_spec.rb b/spec/graphql/mutations/customer_relations/organizations/create_spec.rb
index 738a8d724ab..9be0f5d4289 100644
--- a/spec/graphql/mutations/customer_relations/organizations/create_spec.rb
+++ b/spec/graphql/mutations/customer_relations/organizations/create_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe Mutations::CustomerRelations::Organizations::Create do
it 'raises an error' do
expect { resolve_mutation }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
- .with_message("The resource that you are attempting to access does not exist or you don't have permission to perform this action")
+ .with_message(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
end
end
@@ -46,7 +46,7 @@ RSpec.describe Mutations::CustomerRelations::Organizations::Create do
it 'raises an error' do
expect { resolve_mutation }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
- .with_message('Feature disabled')
+ .with_message("The resource that you are attempting to access does not exist or you don't have permission to perform this action")
end
end
@@ -69,5 +69,5 @@ RSpec.describe Mutations::CustomerRelations::Organizations::Create do
end
end
- specify { expect(described_class).to require_graphql_authorizations(:admin_organization) }
+ specify { expect(described_class).to require_graphql_authorizations(:admin_crm_organization) }
end
diff --git a/spec/graphql/mutations/customer_relations/organizations/update_spec.rb b/spec/graphql/mutations/customer_relations/organizations/update_spec.rb
index 0bc6f184fe3..e3aa8eafe0c 100644
--- a/spec/graphql/mutations/customer_relations/organizations/update_spec.rb
+++ b/spec/graphql/mutations/customer_relations/organizations/update_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Mutations::CustomerRelations::Organizations::Update do
let(:name) { 'GitLab' }
let(:default_rate) { 1000.to_f }
let(:description) { 'VIP' }
- let(:does_not_exist_or_no_permission) { "The resource that you are attempting to access does not exist or you don't have permission to perform this action" }
+ let(:does_not_exist_or_no_permission) { Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR }
let(:organization) { create(:organization, group: group) }
let(:attributes) do
{
@@ -63,11 +63,11 @@ RSpec.describe Mutations::CustomerRelations::Organizations::Update do
it 'raises an error' do
expect { resolve_mutation }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
- .with_message('Feature disabled')
+ .with_message("The resource that you are attempting to access does not exist or you don't have permission to perform this action")
end
end
end
end
- specify { expect(described_class).to require_graphql_authorizations(:admin_organization) }
+ specify { expect(described_class).to require_graphql_authorizations(:admin_crm_organization) }
end
diff --git a/spec/graphql/mutations/discussions/toggle_resolve_spec.rb b/spec/graphql/mutations/discussions/toggle_resolve_spec.rb
index 8c11279a80a..2041b86d6e7 100644
--- a/spec/graphql/mutations/discussions/toggle_resolve_spec.rb
+++ b/spec/graphql/mutations/discussions/toggle_resolve_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe Mutations::Discussions::ToggleResolve do
it 'raises an error if the resource is not accessible to the user' do
expect { subject }.to raise_error(
Gitlab::Graphql::Errors::ResourceNotAvailable,
- "The resource that you are attempting to access does not exist or you don't have permission to perform this action"
+ Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR
)
end
end
@@ -41,7 +41,7 @@ RSpec.describe Mutations::Discussions::ToggleResolve do
it 'raises an error' do
expect { subject }.to raise_error(
Gitlab::Graphql::Errors::ResourceNotAvailable,
- "The resource that you are attempting to access does not exist or you don't have permission to perform this action"
+ Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR
)
end
end
diff --git a/spec/graphql/mutations/environments/canary_ingress/update_spec.rb b/spec/graphql/mutations/environments/canary_ingress/update_spec.rb
index 2715a908f85..48e55828a6b 100644
--- a/spec/graphql/mutations/environments/canary_ingress/update_spec.rb
+++ b/spec/graphql/mutations/environments/canary_ingress/update_spec.rb
@@ -60,7 +60,7 @@ RSpec.describe Mutations::Environments::CanaryIngress::Update do
let(:user) { reporter }
it 'raises an error' do
- expect { subject }.to raise_error("The resource that you are attempting to access does not exist or you don't have permission to perform this action")
+ expect { subject }.to raise_error(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
end
end
end
diff --git a/spec/graphql/mutations/merge_requests/set_wip_spec.rb b/spec/graphql/mutations/merge_requests/set_wip_spec.rb
deleted file mode 100644
index fae9c4f7fe0..00000000000
--- a/spec/graphql/mutations/merge_requests/set_wip_spec.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Mutations::MergeRequests::SetWip do
- let(:merge_request) { create(:merge_request) }
- let(:user) { create(:user) }
-
- subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
-
- specify { expect(described_class).to require_graphql_authorizations(:update_merge_request) }
-
- describe '#resolve' do
- let(:wip) { true }
- let(:mutated_merge_request) { subject[:merge_request] }
-
- subject { mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, wip: wip) }
-
- it_behaves_like 'permission level for merge request mutation is correctly verified'
-
- context 'when the user can update the merge request' do
- before do
- merge_request.project.add_developer(user)
- end
-
- it 'returns the merge request as a wip' do
- expect(mutated_merge_request).to eq(merge_request)
- expect(mutated_merge_request).to be_work_in_progress
- expect(subject[:errors]).to be_empty
- end
-
- it 'returns errors merge request could not be updated' do
- # Make the merge request invalid
- merge_request.allow_broken = true
- merge_request.update!(source_project: nil)
-
- expect(subject[:errors]).not_to be_empty
- end
-
- context 'when passing wip as false' do
- let(:wip) { false }
-
- it 'removes `wip` from the title' do
- merge_request.update!(title: "WIP: working on it")
-
- expect(mutated_merge_request).not_to be_work_in_progress
- end
-
- it 'does not do anything if the title did not start with wip' do
- expect(mutated_merge_request).not_to be_work_in_progress
- end
- end
- end
- end
-end
diff --git a/spec/graphql/mutations/notes/reposition_image_diff_note_spec.rb b/spec/graphql/mutations/notes/reposition_image_diff_note_spec.rb
index e78f755d5c7..39794a070c6 100644
--- a/spec/graphql/mutations/notes/reposition_image_diff_note_spec.rb
+++ b/spec/graphql/mutations/notes/reposition_image_diff_note_spec.rb
@@ -29,7 +29,7 @@ RSpec.describe Mutations::Notes::RepositionImageDiffNote do
it 'raises an error if the resource is not accessible to the user' do
expect { subject }.to raise_error(
Gitlab::Graphql::Errors::ResourceNotAvailable,
- "The resource that you are attempting to access does not exist or you don't have permission to perform this action"
+ Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR
)
end
end
diff --git a/spec/graphql/mutations/releases/delete_spec.rb b/spec/graphql/mutations/releases/delete_spec.rb
index d97f839ce87..9934aea0031 100644
--- a/spec/graphql/mutations/releases/delete_spec.rb
+++ b/spec/graphql/mutations/releases/delete_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Mutations::Releases::Delete do
shared_examples 'unauthorized or not found error' do
it 'raises a Gitlab::Graphql::Errors::ResourceNotAvailable error' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, "The resource that you are attempting to access does not exist or you don't have permission to perform this action")
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
end
end
diff --git a/spec/graphql/mutations/releases/update_spec.rb b/spec/graphql/mutations/releases/update_spec.rb
index 5ee63ac4dc2..9fae703b85a 100644
--- a/spec/graphql/mutations/releases/update_spec.rb
+++ b/spec/graphql/mutations/releases/update_spec.rb
@@ -232,7 +232,7 @@ RSpec.describe Mutations::Releases::Update do
let(:mutation_arguments) { super().merge(project_path: 'not/a/real/path') }
it 'raises an error' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, "The resource that you are attempting to access does not exist or you don't have permission to perform this action")
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
end
end
end
@@ -242,7 +242,7 @@ RSpec.describe Mutations::Releases::Update do
let(:current_user) { reporter }
it 'raises an error' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, "The resource that you are attempting to access does not exist or you don't have permission to perform this action")
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
end
end
end
diff --git a/spec/graphql/mutations/security/ci_configuration/configure_sast_iac_spec.rb b/spec/graphql/mutations/security/ci_configuration/configure_sast_iac_spec.rb
new file mode 100644
index 00000000000..f16d504a4ae
--- /dev/null
+++ b/spec/graphql/mutations/security/ci_configuration/configure_sast_iac_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Security::CiConfiguration::ConfigureSastIac do
+ include GraphqlHelpers
+
+ let(:service) { ::Security::CiConfiguration::SastIacCreateService }
+
+ subject { resolve(described_class, args: { project_path: project.full_path }, ctx: { current_user: user }) }
+
+ include_examples 'graphql mutations security ci configuration'
+end
diff --git a/spec/graphql/resolvers/concerns/resolves_groups_spec.rb b/spec/graphql/resolvers/concerns/resolves_groups_spec.rb
new file mode 100644
index 00000000000..bfbbae29e92
--- /dev/null
+++ b/spec/graphql/resolvers/concerns/resolves_groups_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ResolvesGroups do
+ include GraphqlHelpers
+ include AfterNextHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:groups) { create_pair(:group) }
+
+ let_it_be(:resolver) do
+ Class.new(Resolvers::BaseResolver) do
+ include ResolvesGroups
+ type Types::GroupType, null: true
+ end
+ end
+
+ let_it_be(:query_type) do
+ query_factory do |query|
+ query.field :groups,
+ Types::GroupType.connection_type,
+ null: true,
+ resolver: resolver
+ end
+ end
+
+ let_it_be(:lookahead_fields) do
+ <<~FIELDS
+ contacts { nodes { id } }
+ containerRepositoriesCount
+ customEmoji { nodes { id } }
+ fullPath
+ organizations { nodes { id } }
+ path
+ dependencyProxyBlobCount
+ dependencyProxyBlobs { nodes { fileName } }
+ dependencyProxyImageCount
+ dependencyProxyImageTtlPolicy { enabled }
+ dependencyProxySetting { enabled }
+ FIELDS
+ end
+
+ it 'avoids N+1 queries on the fields marked with lookahead' do
+ group_ids = groups.map(&:id)
+
+ allow_next(resolver).to receive(:resolve_groups).and_return(Group.id_in(group_ids))
+ # Prevent authorization queries from affecting the test.
+ allow(Ability).to receive(:allowed?).and_return(true)
+
+ single_group_query = ActiveRecord::QueryRecorder.new do
+ data = query_groups(limit: 1)
+ expect(data.size).to eq(1)
+ end
+
+ multi_group_query = -> {
+ data = query_groups(limit: 2)
+ expect(data.size).to eq(2)
+ }
+
+ expect { multi_group_query.call }.not_to exceed_query_limit(single_group_query)
+ end
+
+ def query_groups(limit:)
+ query_string = "{ groups(first: #{limit}) { nodes { id #{lookahead_fields} } } }"
+
+ data = execute_query(query_type, graphql: query_string)
+
+ graphql_dig_at(data, :data, :groups, :nodes)
+ end
+end
diff --git a/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb b/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb
index 865e892b12d..3fcfa967452 100644
--- a/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb
+++ b/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb
@@ -20,23 +20,37 @@ RSpec.describe ResolvesPipelines do
let_it_be(:project) { create(:project, :private) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
let_it_be(:failed_pipeline) { create(:ci_pipeline, :failed, project: project) }
+ let_it_be(:success_pipeline) { create(:ci_pipeline, :success, project: project) }
let_it_be(:ref_pipeline) { create(:ci_pipeline, project: project, ref: 'awesome-feature') }
let_it_be(:sha_pipeline) { create(:ci_pipeline, project: project, sha: 'deadbeef') }
+ let_it_be(:all_pipelines) do
+ [
+ pipeline,
+ failed_pipeline,
+ success_pipeline,
+ ref_pipeline,
+ sha_pipeline
+ ]
+ end
before do
project.add_developer(current_user)
end
- it { is_expected.to have_graphql_arguments(:status, :ref, :sha, :source) }
+ it { is_expected.to have_graphql_arguments(:status, :scope, :ref, :sha, :source) }
it 'finds all pipelines' do
- expect(resolve_pipelines).to contain_exactly(pipeline, failed_pipeline, ref_pipeline, sha_pipeline)
+ expect(resolve_pipelines).to contain_exactly(*all_pipelines)
end
it 'allows filtering by status' do
expect(resolve_pipelines(status: 'failed')).to contain_exactly(failed_pipeline)
end
+ it 'allows filtering by scope' do
+ expect(resolve_pipelines(scope: 'finished')).to contain_exactly(failed_pipeline, success_pipeline)
+ end
+
it 'allows filtering by ref' do
expect(resolve_pipelines(ref: 'awesome-feature')).to contain_exactly(ref_pipeline)
end
@@ -54,7 +68,7 @@ RSpec.describe ResolvesPipelines do
end
it 'does not filter by source' do
- expect(resolve_pipelines(source: 'web')).to contain_exactly(pipeline, failed_pipeline, ref_pipeline, sha_pipeline, source_pipeline)
+ expect(resolve_pipelines(source: 'web')).to contain_exactly(*all_pipelines, source_pipeline)
end
end
@@ -64,7 +78,7 @@ RSpec.describe ResolvesPipelines do
end
it 'returns all the pipelines' do
- expect(resolve_pipelines).to contain_exactly(pipeline, failed_pipeline, ref_pipeline, sha_pipeline, source_pipeline)
+ expect(resolve_pipelines).to contain_exactly(*all_pipelines, source_pipeline)
end
end
end
diff --git a/spec/graphql/resolvers/group_issues_resolver_spec.rb b/spec/graphql/resolvers/group_issues_resolver_spec.rb
index 463cdca699b..e17429560ac 100644
--- a/spec/graphql/resolvers/group_issues_resolver_spec.rb
+++ b/spec/graphql/resolvers/group_issues_resolver_spec.rb
@@ -29,15 +29,72 @@ RSpec.describe Resolvers::GroupIssuesResolver do
describe '#resolve' do
it 'finds all group issues' do
- result = resolve(described_class, obj: group, ctx: { current_user: current_user })
-
- expect(result).to contain_exactly(issue1, issue2, issue3)
+ expect(resolve_issues).to contain_exactly(issue1, issue2, issue3)
end
it 'finds all group and subgroup issues' do
- result = resolve(described_class, obj: group, args: { include_subgroups: true }, ctx: { current_user: current_user })
+ result = resolve_issues(include_subgroups: true)
expect(result).to contain_exactly(issue1, issue2, issue3, subissue1, subissue2, subissue3)
end
+
+ it 'returns issues without the specified issue_type' do
+ result = resolve_issues(not: { types: ['issue'] })
+
+ expect(result).to contain_exactly(issue1)
+ end
+
+ context 'confidential issues' do
+ let_it_be(:confidential_issue1) { create(:issue, project: project, confidential: true) }
+ let_it_be(:confidential_issue2) { create(:issue, project: other_project, confidential: true) }
+
+ context "when user is allowed to view confidential issues" do
+ it 'returns all viewable issues by default' do
+ expect(resolve_issues).to contain_exactly(issue1, issue2, issue3, confidential_issue1, confidential_issue2)
+ end
+
+ context 'filtering for confidential issues' do
+ it 'returns only the non-confidential issues for the group when filter is set to false' do
+ expect(resolve_issues({ confidential: false })).to contain_exactly(issue1, issue2, issue3)
+ end
+
+ it "returns only the confidential issues for the group when filter is set to true" do
+ expect(resolve_issues({ confidential: true })).to contain_exactly(confidential_issue1, confidential_issue2)
+ end
+ end
+ end
+
+ context "when user is not allowed to see confidential issues" do
+ before do
+ group.add_guest(current_user)
+ end
+
+ it 'returns all viewable issues by default' do
+ expect(resolve_issues).to contain_exactly(issue1, issue2, issue3)
+ end
+
+ context 'filtering for confidential issues' do
+ it 'does not return the confidential issues when filter is set to false' do
+ expect(resolve_issues({ confidential: false })).to contain_exactly(issue1, issue2, issue3)
+ end
+
+ it 'does not return the confidential issues when filter is set to true' do
+ expect(resolve_issues({ confidential: true })).to be_empty
+ end
+ end
+ end
+ end
+
+ context 'release_tag filter' do
+ it 'returns an error when trying to filter by negated release_tag' do
+ expect do
+ resolve_issues(not: { release_tag: ['v1.0'] })
+ end.to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'releaseTag filter is not allowed when parent is a group.')
+ end
+ end
+ end
+
+ def resolve_issues(args = {}, context = { current_user: current_user })
+ resolve(described_class, obj: group, 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 9897e697009..3c892214aaf 100644
--- a/spec/graphql/resolvers/issues_resolver_spec.rb
+++ b/spec/graphql/resolvers/issues_resolver_spec.rb
@@ -26,14 +26,7 @@ RSpec.describe Resolvers::IssuesResolver do
expect(described_class).to have_nullable_graphql_type(Types::IssueType.connection_type)
end
- shared_context 'filtering for confidential issues' do
- let_it_be(:confidential_issue1) { create(:issue, project: project, confidential: true) }
- let_it_be(:confidential_issue2) { create(:issue, project: other_project, confidential: true) }
- end
-
context "with a project" do
- let(:obj) { project }
-
before_all do
project.add_developer(current_user)
project.add_reporter(reporter)
@@ -112,6 +105,54 @@ RSpec.describe Resolvers::IssuesResolver do
end
end
+ describe 'filter by release' do
+ let_it_be(:milestone1) { create(:milestone, project: project, start_date: 1.day.from_now, title: 'Version 1') }
+ let_it_be(:milestone2) { create(:milestone, project: project, start_date: 1.day.from_now, title: 'Version 2') }
+ let_it_be(:milestone3) { create(:milestone, project: project, start_date: 1.day.from_now, title: 'Version 3') }
+ let_it_be(:release1) { create(:release, tag: 'v1.0', milestones: [milestone1], project: project) }
+ let_it_be(:release2) { create(:release, tag: 'v2.0', milestones: [milestone2], project: project) }
+ let_it_be(:release3) { create(:release, tag: 'v3.0', milestones: [milestone3], project: project) }
+ let_it_be(:release_issue1) { create(:issue, project: project, milestone: milestone1) }
+ let_it_be(:release_issue2) { create(:issue, project: project, milestone: milestone2) }
+ let_it_be(:release_issue3) { create(:issue, project: project, milestone: milestone3) }
+
+ describe 'filter by release_tag' do
+ it 'returns all issues associated with the specified tags' do
+ expect(resolve_issues(release_tag: [release1.tag, release3.tag])).to contain_exactly(release_issue1, release_issue3)
+ end
+
+ context 'when release_tag_wildcard_id is also provided' do
+ it 'raises a mutually eclusive argument error' do
+ expect do
+ resolve_issues(release_tag: [release1.tag], release_tag_wildcard_id: 'ANY')
+ end.to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'only one of [releaseTag, releaseTagWildcardId] arguments is allowed at the same time.')
+ end
+ end
+ end
+
+ describe 'filter by negated release_tag' do
+ it 'returns all issues not associated with the specified tags' do
+ expect(resolve_issues(not: { release_tag: [release1.tag, release3.tag] })).to contain_exactly(release_issue2)
+ end
+ end
+
+ describe 'filter by release_tag_wildcard_id' do
+ subject { resolve_issues(release_tag_wildcard_id: wildcard_id) }
+
+ context 'when filtering by ANY' do
+ let(:wildcard_id) { 'ANY' }
+
+ it { is_expected.to contain_exactly(release_issue1, release_issue2, release_issue3) }
+ end
+
+ context 'when filtering by NONE' do
+ let(:wildcard_id) { 'NONE' }
+
+ it { is_expected.to contain_exactly(issue1, issue2) }
+ end
+ end
+ end
+
it 'filters by two assignees' do
assignee2 = create(:user)
issue2.update!(assignees: [assignee, assignee2])
@@ -230,7 +271,8 @@ RSpec.describe Resolvers::IssuesResolver do
end
context 'confidential issues' do
- include_context 'filtering for confidential issues'
+ let_it_be(:confidential_issue1) { create(:issue, project: project, confidential: true) }
+ let_it_be(:confidential_issue2) { create(:issue, project: other_project, confidential: true) }
context "when user is allowed to view confidential issues" do
it 'returns all viewable issues by default' do
@@ -561,64 +603,6 @@ RSpec.describe Resolvers::IssuesResolver do
end
end
- context "with a group" do
- let(:obj) { group }
-
- before do
- group.add_developer(current_user)
- end
-
- describe '#resolve' do
- it 'finds all group issues' do
- expect(resolve_issues).to contain_exactly(issue1, issue2, issue3)
- end
-
- it 'returns issues without the specified issue_type' do
- expect(resolve_issues({ not: { types: ['issue'] } })).to contain_exactly(issue1)
- end
-
- context "confidential issues" do
- include_context 'filtering for confidential issues'
-
- context "when user is allowed to view confidential issues" do
- it 'returns all viewable issues by default' do
- expect(resolve_issues).to contain_exactly(issue1, issue2, issue3, confidential_issue1, confidential_issue2)
- end
-
- context 'filtering for confidential issues' do
- it 'returns only the non-confidential issues for the group when filter is set to false' do
- expect(resolve_issues({ confidential: false })).to contain_exactly(issue1, issue2, issue3)
- end
-
- it "returns only the confidential issues for the group when filter is set to true" do
- expect(resolve_issues({ confidential: true })).to contain_exactly(confidential_issue1, confidential_issue2)
- end
- end
- end
-
- context "when user is not allowed to see confidential issues" do
- before do
- group.add_guest(current_user)
- end
-
- it 'returns all viewable issues by default' do
- expect(resolve_issues).to contain_exactly(issue1, issue2, issue3)
- end
-
- context 'filtering for confidential issues' do
- it 'does not return the confidential issues when filter is set to false' do
- expect(resolve_issues({ confidential: false })).to contain_exactly(issue1, issue2, issue3)
- end
-
- it 'does not return the confidential issues when filter is set to true' do
- expect(resolve_issues({ confidential: true })).to be_empty
- end
- end
- end
- end
- end
- end
-
context "when passing a non existent, batch loaded project" do
let!(:project) do
BatchLoader::GraphQL.for("non-existent-path").batch do |_fake_paths, loader, _|
@@ -626,8 +610,6 @@ RSpec.describe Resolvers::IssuesResolver do
end
end
- let(:obj) { project }
-
it "returns nil without breaking" do
expect(resolve_issues(iids: ["don't", "break"])).to be_empty
end
@@ -648,6 +630,6 @@ RSpec.describe Resolvers::IssuesResolver do
end
def resolve_issues(args = {}, context = { current_user: current_user })
- resolve(described_class, obj: obj, args: args, ctx: context)
+ resolve(described_class, obj: project, 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 a897acf7eba..a931b0a3f77 100644
--- a/spec/graphql/resolvers/merge_requests_resolver_spec.rb
+++ b/spec/graphql/resolvers/merge_requests_resolver_spec.rb
@@ -218,6 +218,54 @@ RSpec.describe Resolvers::MergeRequestsResolver do
end
end
+ context 'with created_after and created_before arguments' do
+ before do
+ merge_request_1.update!(created_at: 4.days.ago)
+ end
+
+ let(:all_mrs) do
+ [merge_request_1, merge_request_2, merge_request_3, merge_request_4, merge_request_5, merge_request_6, merge_request_with_milestone]
+ end
+
+ it 'returns merge requests created within a given period' do
+ result = resolve_mr(project, created_after: 5.days.ago, created_before: 2.days.ago)
+
+ expect(result).to contain_exactly(
+ merge_request_1
+ )
+ end
+
+ it 'returns some values filtered with created_before' do
+ result = resolve_mr(project, created_before: 1.day.ago)
+
+ expect(result).to contain_exactly(merge_request_1)
+ end
+
+ it 'returns some values filtered with created_after' do
+ result = resolve_mr(project, created_after: 3.days.ago)
+
+ expect(result).to match_array(all_mrs - [merge_request_1])
+ end
+
+ it 'does not return anything for dates (even in the future) not matching any MRs' do
+ result = resolve_mr(project, created_after: 5.days.from_now)
+
+ expect(result).to be_empty
+ end
+
+ it 'does not return anything for dates not matching any MRs' do
+ result = resolve_mr(project, created_before: 15.days.ago)
+
+ expect(result).to be_empty
+ end
+
+ it 'does not return any values for an impossible set' do
+ result = resolve_mr(project, created_after: 5.days.ago, created_before: 6.days.ago)
+
+ expect(result).to be_empty
+ end
+ end
+
context 'with milestone argument' do
it 'filters merge requests by milestone title' do
result = resolve_mr(project, milestone_title: milestone.title)
diff --git a/spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb b/spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb
index 75b9be7dfe7..c6d8c518fb7 100644
--- a/spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb
+++ b/spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb
@@ -90,7 +90,10 @@ RSpec.describe Resolvers::Projects::JiraProjectsResolver do
end
it 'raises failure error' do
- expect { resolve_jira_projects }.to raise_error('An error occurred while requesting data from Jira: Some failure. Check your Jira integration configuration and try again.')
+ config_docs_link_url = Rails.application.routes.url_helpers.help_page_path('integration/jira/configure')
+ docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: config_docs_link_url }
+ error_message = 'An error occurred while requesting data from Jira: Some failure. Check your %{docs_link_start}Jira integration configuration</a> and try again.' % { docs_link_start: docs_link_start }
+ expect { resolve_jira_projects }.to raise_error(error_message)
end
end
end
diff --git a/spec/graphql/resolvers/timelog_resolver_spec.rb b/spec/graphql/resolvers/timelog_resolver_spec.rb
index f45f528fe7e..9b3f555071e 100644
--- a/spec/graphql/resolvers/timelog_resolver_spec.rb
+++ b/spec/graphql/resolvers/timelog_resolver_spec.rb
@@ -11,6 +11,8 @@ RSpec.describe Resolvers::TimelogResolver do
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:error_class) { Gitlab::Graphql::Errors::ArgumentError }
+ let(:timelogs) { resolve_timelogs(**args) }
+
specify do
expect(described_class).to have_non_null_graphql_type(::Types::TimelogType.connection_type)
end
@@ -24,8 +26,6 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { { start_time: 6.days.ago, end_time: 2.days.ago.noon } }
it 'finds all timelogs within given dates' do
- timelogs = resolve_timelogs(**args)
-
expect(timelogs).to contain_exactly(timelog1)
end
@@ -33,8 +33,6 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { {} }
it 'finds all timelogs' do
- timelogs = resolve_timelogs(**args)
-
expect(timelogs).to contain_exactly(timelog1, timelog2, timelog3)
end
end
@@ -43,8 +41,6 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { { start_time: 2.days.ago.noon } }
it 'finds timelogs after the start_time' do
- timelogs = resolve_timelogs(**args)
-
expect(timelogs).to contain_exactly(timelog2)
end
end
@@ -53,8 +49,6 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { { end_time: 2.days.ago.noon } }
it 'finds timelogs before the end_time' do
- timelogs = resolve_timelogs(**args)
-
expect(timelogs).to contain_exactly(timelog1, timelog3)
end
end
@@ -63,8 +57,6 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { { start_time: 6.days.ago, end_date: 2.days.ago } }
it 'finds timelogs until the end of day of end_date' do
- timelogs = resolve_timelogs(**args)
-
expect(timelogs).to contain_exactly(timelog1, timelog2)
end
end
@@ -73,8 +65,6 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { { start_date: 6.days.ago, end_time: 2.days.ago.noon } }
it 'finds all timelogs within start_date and end_time' do
- timelogs = resolve_timelogs(**args)
-
expect(timelogs).to contain_exactly(timelog1)
end
end
@@ -96,7 +86,7 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { { start_time: 6.days.ago, start_date: 6.days.ago } }
it 'returns correct error' do
- expect { resolve_timelogs(**args) }
+ expect { timelogs }
.to raise_error(error_class, /Provide either a start date or time, but not both/)
end
end
@@ -105,7 +95,7 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { { end_time: 2.days.ago, end_date: 2.days.ago } }
it 'returns correct error' do
- expect { resolve_timelogs(**args) }
+ expect { timelogs }
.to raise_error(error_class, /Provide either an end date or time, but not both/)
end
end
@@ -114,14 +104,14 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { { start_time: 2.days.ago, end_time: 6.days.ago } }
it 'returns correct error' do
- expect { resolve_timelogs(**args) }
+ expect { timelogs }
.to raise_error(error_class, /Start argument must be before End argument/)
end
end
end
end
- shared_examples "with a group" do
+ shared_examples 'with a group' do
let_it_be(:short_time_ago) { 5.days.ago.beginning_of_day }
let_it_be(:medium_time_ago) { 15.days.ago.beginning_of_day }
@@ -141,8 +131,6 @@ RSpec.describe Resolvers::TimelogResolver do
end
it 'finds all timelogs within given dates' do
- timelogs = resolve_timelogs(**args)
-
expect(timelogs).to contain_exactly(timelog1)
end
@@ -150,8 +138,6 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { { start_date: short_time_ago } }
it 'finds timelogs until the end of day of end_date' do
- timelogs = resolve_timelogs(**args)
-
expect(timelogs).to contain_exactly(timelog1, timelog2)
end
end
@@ -160,8 +146,6 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { { end_date: medium_time_ago } }
it 'finds timelogs until the end of day of end_date' do
- timelogs = resolve_timelogs(**args)
-
expect(timelogs).to contain_exactly(timelog3)
end
end
@@ -170,8 +154,6 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { { start_time: short_time_ago, end_date: short_time_ago } }
it 'finds timelogs until the end of day of end_date' do
- timelogs = resolve_timelogs(**args)
-
expect(timelogs).to contain_exactly(timelog1, timelog2)
end
end
@@ -180,8 +162,6 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { { start_date: short_time_ago, end_time: short_time_ago.noon } }
it 'finds all timelogs within start_date and end_time' do
- timelogs = resolve_timelogs(**args)
-
expect(timelogs).to contain_exactly(timelog1)
end
end
@@ -191,7 +171,7 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { { start_time: short_time_ago, start_date: short_time_ago } }
it 'returns correct error' do
- expect { resolve_timelogs(**args) }
+ expect { timelogs }
.to raise_error(error_class, /Provide either a start date or time, but not both/)
end
end
@@ -200,7 +180,7 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { { end_time: short_time_ago, end_date: short_time_ago } }
it 'returns correct error' do
- expect { resolve_timelogs(**args) }
+ expect { timelogs }
.to raise_error(error_class, /Provide either an end date or time, but not both/)
end
end
@@ -209,14 +189,14 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { { start_time: short_time_ago, end_time: medium_time_ago } }
it 'returns correct error' do
- expect { resolve_timelogs(**args) }
+ expect { timelogs }
.to raise_error(error_class, /Start argument must be before End argument/)
end
end
end
end
- shared_examples "with a user" do
+ shared_examples 'with a user' do
let_it_be(:short_time_ago) { 5.days.ago.beginning_of_day }
let_it_be(:medium_time_ago) { 15.days.ago.beginning_of_day }
@@ -228,20 +208,18 @@ RSpec.describe Resolvers::TimelogResolver do
let_it_be(:timelog3) { create(:merge_request_timelog, merge_request: merge_request, user: current_user) }
it 'blah' do
- timelogs = resolve_timelogs(**args)
-
expect(timelogs).to contain_exactly(timelog1, timelog3)
end
end
- context "on a project" do
+ context 'on a project' do
let(:object) { project }
let(:extra_args) { {} }
it_behaves_like 'with a project'
end
- context "with a project filter" do
+ context 'with a project filter' do
let(:object) { nil }
let(:extra_args) { { project_id: project.to_global_id } }
@@ -285,8 +263,6 @@ RSpec.describe Resolvers::TimelogResolver do
let(:extra_args) { {} }
it 'pagination returns `default_max_page_size` and sets `has_next_page` true' do
- timelogs = resolve_timelogs(**args)
-
expect(timelogs.items.count).to be(100)
expect(timelogs.has_next_page).to be(true)
end
@@ -298,7 +274,7 @@ RSpec.describe Resolvers::TimelogResolver do
let(:extra_args) { {} }
it 'returns correct error' do
- expect { resolve_timelogs(**args) }
+ expect { timelogs }
.to raise_error(error_class, /Provide at least one argument/)
end
end
diff --git a/spec/graphql/resolvers/topics_resolver_spec.rb b/spec/graphql/resolvers/topics_resolver_spec.rb
new file mode 100644
index 00000000000..3ff1dabc927
--- /dev/null
+++ b/spec/graphql/resolvers/topics_resolver_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::TopicsResolver do
+ include GraphqlHelpers
+
+ describe '#resolve' do
+ let!(:topic1) { create(:topic, name: 'GitLab', total_projects_count: 1) }
+ let!(:topic2) { create(:topic, name: 'git', total_projects_count: 2) }
+ let!(:topic3) { create(:topic, name: 'topic3', total_projects_count: 3) }
+
+ it 'finds all topics' do
+ expect(resolve_topics).to eq([topic3, topic2, topic1])
+ end
+
+ context 'with search' do
+ it 'searches environment by name' do
+ expect(resolve_topics(search: 'git')).to eq([topic2, topic1])
+ end
+
+ context 'when the search term does not match any topic' do
+ it 'is empty' do
+ expect(resolve_topics(search: 'nonsense')).to be_empty
+ end
+ end
+ end
+ end
+
+ def resolve_topics(args = {})
+ resolve(described_class, args: args)
+ end
+end
diff --git a/spec/graphql/types/alert_management/prometheus_integration_type_spec.rb b/spec/graphql/types/alert_management/prometheus_integration_type_spec.rb
index 31cf94aef44..bfb6958e327 100644
--- a/spec/graphql/types/alert_management/prometheus_integration_type_spec.rb
+++ b/spec/graphql/types/alert_management/prometheus_integration_type_spec.rb
@@ -50,7 +50,7 @@ RSpec.describe GitlabSchema.types['AlertManagementPrometheusIntegration'] do
describe 'a group integration' do
let_it_be(:group) { create(:group) }
- let_it_be(:integration) { create(:prometheus_integration, project: nil, group: group) }
+ let_it_be(:integration) { create(:prometheus_integration, :group, group: group) }
# Since it is impossible to authorize the parent here, given that the
# project is nil, all fields should be redacted:
diff --git a/spec/graphql/types/ci/job_artifact_type_spec.rb b/spec/graphql/types/ci/job_artifact_type_spec.rb
index d4dc5ef214d..58b5f9cfcb7 100644
--- a/spec/graphql/types/ci/job_artifact_type_spec.rb
+++ b/spec/graphql/types/ci/job_artifact_type_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['CiJobArtifact'] do
it 'has the correct fields' do
- expected_fields = [:download_path, :file_type]
+ expected_fields = [:download_path, :file_type, :name]
expect(described_class).to have_graphql_fields(*expected_fields)
end
diff --git a/spec/graphql/types/ci/pipeline_scope_enum_spec.rb b/spec/graphql/types/ci/pipeline_scope_enum_spec.rb
new file mode 100644
index 00000000000..9dc6e5c6fae
--- /dev/null
+++ b/spec/graphql/types/ci/pipeline_scope_enum_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Ci::PipelineScopeEnum do
+ it 'exposes all pipeline scopes' do
+ expect(described_class.values.keys).to contain_exactly(
+ *::Ci::PipelinesFinder::ALLOWED_SCOPES.keys.map(&:to_s)
+ )
+ end
+end
diff --git a/spec/graphql/types/ci/pipeline_status_enum_spec.rb b/spec/graphql/types/ci/pipeline_status_enum_spec.rb
new file mode 100644
index 00000000000..2d6683c6384
--- /dev/null
+++ b/spec/graphql/types/ci/pipeline_status_enum_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Ci::PipelineStatusEnum do
+ it 'exposes all pipeline states' do
+ expect(described_class.values.keys).to contain_exactly(
+ *::Ci::Pipeline.all_state_names.map(&:to_s).map(&:upcase)
+ )
+ end
+end
diff --git a/spec/graphql/types/ci/pipeline_type_spec.rb b/spec/graphql/types/ci/pipeline_type_spec.rb
index 8c849114cf6..58724524785 100644
--- a/spec/graphql/types/ci/pipeline_type_spec.rb
+++ b/spec/graphql/types/ci/pipeline_type_spec.rb
@@ -12,8 +12,8 @@ RSpec.describe Types::Ci::PipelineType do
id iid sha before_sha complete status detailed_status config_source
duration queued_duration
coverage created_at updated_at started_at finished_at committed_at
- stages user retryable cancelable jobs source_job job downstream
- upstream path project active user_permissions warnings commit_path uses_needs
+ stages user retryable cancelable jobs source_job job job_artifacts downstream
+ upstream path project active user_permissions warnings commit commit_path uses_needs
test_report_summary test_suite ref
]
diff --git a/spec/graphql/types/commit_type_spec.rb b/spec/graphql/types/commit_type_spec.rb
index b43693e5804..2f74ce81761 100644
--- a/spec/graphql/types/commit_type_spec.rb
+++ b/spec/graphql/types/commit_type_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe GitlabSchema.types['Commit'] do
it 'contains attributes related to commit' do
expect(described_class).to have_graphql_fields(
- :id, :sha, :short_id, :title, :description, :description_html, :message, :title_html, :authored_date,
+ :id, :sha, :short_id, :title, :full_title, :full_title_html, :description, :description_html, :message, :title_html, :authored_date,
:author_name, :author_gravatar, :author, :web_url, :web_path,
:pipelines, :signature_html
)
diff --git a/spec/graphql/types/customer_relations/contact_type_spec.rb b/spec/graphql/types/customer_relations/contact_type_spec.rb
index a51ee705fb0..bb447f405b6 100644
--- a/spec/graphql/types/customer_relations/contact_type_spec.rb
+++ b/spec/graphql/types/customer_relations/contact_type_spec.rb
@@ -7,5 +7,5 @@ RSpec.describe GitlabSchema.types['CustomerRelationsContact'] do
it { expect(described_class.graphql_name).to eq('CustomerRelationsContact') }
it { expect(described_class).to have_graphql_fields(fields) }
- it { expect(described_class).to require_graphql_authorizations(:read_contact) }
+ it { expect(described_class).to require_graphql_authorizations(:read_crm_contact) }
end
diff --git a/spec/graphql/types/customer_relations/organization_type_spec.rb b/spec/graphql/types/customer_relations/organization_type_spec.rb
index 2562748477c..93844df1239 100644
--- a/spec/graphql/types/customer_relations/organization_type_spec.rb
+++ b/spec/graphql/types/customer_relations/organization_type_spec.rb
@@ -7,5 +7,5 @@ RSpec.describe GitlabSchema.types['CustomerRelationsOrganization'] do
it { expect(described_class.graphql_name).to eq('CustomerRelationsOrganization') }
it { expect(described_class).to have_graphql_fields(fields) }
- it { expect(described_class).to require_graphql_authorizations(:read_organization) }
+ it { expect(described_class).to require_graphql_authorizations(:read_crm_organization) }
end
diff --git a/spec/graphql/types/dependency_proxy/manifest_type_spec.rb b/spec/graphql/types/dependency_proxy/manifest_type_spec.rb
index 18cc89adfcb..b251ca63c4f 100644
--- a/spec/graphql/types/dependency_proxy/manifest_type_spec.rb
+++ b/spec/graphql/types/dependency_proxy/manifest_type_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['DependencyProxyManifest'] do
it 'includes dependency proxy manifest fields' do
expected_fields = %w[
- file_name image_name size created_at updated_at digest
+ id file_name image_name size created_at updated_at digest
]
expect(described_class).to include_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/evidence_type_spec.rb b/spec/graphql/types/evidence_type_spec.rb
index 92134e74d51..be85724eac5 100644
--- a/spec/graphql/types/evidence_type_spec.rb
+++ b/spec/graphql/types/evidence_type_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe GitlabSchema.types['ReleaseEvidence'] do
- it { expect(described_class).to require_graphql_authorizations(:download_code) }
+ specify { expect(described_class).to require_graphql_authorizations(:read_release_evidence) }
it 'has the expected fields' do
expected_fields = %w[
diff --git a/spec/graphql/types/merge_request_review_state_enum_spec.rb b/spec/graphql/types/merge_request_review_state_enum_spec.rb
index 486e1c4f502..407a1ae3c1f 100644
--- a/spec/graphql/types/merge_request_review_state_enum_spec.rb
+++ b/spec/graphql/types/merge_request_review_state_enum_spec.rb
@@ -12,6 +12,10 @@ RSpec.describe GitlabSchema.types['MergeRequestReviewState'] do
'UNREVIEWED' => have_attributes(
description: 'The merge request is unreviewed.',
value: 'unreviewed'
+ ),
+ 'ATTENTION_REQUESTED' => have_attributes(
+ description: 'The merge request is attention_requested.',
+ value: 'attention_requested'
)
)
end
diff --git a/spec/graphql/types/merge_request_type_spec.rb b/spec/graphql/types/merge_request_type_spec.rb
index bc3ccb0d9ba..b17b7c32289 100644
--- a/spec/graphql/types/merge_request_type_spec.rb
+++ b/spec/graphql/types/merge_request_type_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe GitlabSchema.types['MergeRequest'] do
notes discussions user_permissions id iid title title_html description
description_html state created_at updated_at source_project target_project
project project_id source_project_id target_project_id source_branch
- target_branch work_in_progress draft merge_when_pipeline_succeeds diff_head_sha
+ target_branch draft merge_when_pipeline_succeeds diff_head_sha
merge_commit_sha user_notes_count user_discussions_count should_remove_source_branch
diff_refs diff_stats diff_stats_summary
force_remove_source_branch
diff --git a/spec/graphql/types/mutation_type_spec.rb b/spec/graphql/types/mutation_type_spec.rb
index c1a5c93c85b..95d835c88cf 100644
--- a/spec/graphql/types/mutation_type_spec.rb
+++ b/spec/graphql/types/mutation_type_spec.rb
@@ -3,14 +3,6 @@
require 'spec_helper'
RSpec.describe Types::MutationType do
- it 'is expected to have the deprecated MergeRequestSetWip' do
- field = get_field('MergeRequestSetWip')
-
- expect(field).to be_present
- expect(field.deprecation_reason).to be_present
- expect(field.resolver).to eq(Mutations::MergeRequests::SetWip)
- end
-
it 'is expected to have the MergeRequestSetDraft' do
expect(described_class).to have_graphql_mutation(Mutations::MergeRequests::SetDraft)
end
diff --git a/spec/graphql/types/packages/helm/dependency_type_spec.rb b/spec/graphql/types/packages/helm/dependency_type_spec.rb
new file mode 100644
index 00000000000..2047205275f
--- /dev/null
+++ b/spec/graphql/types/packages/helm/dependency_type_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['PackageHelmDependencyType'] do
+ it { expect(described_class.graphql_name).to eq('PackageHelmDependencyType') }
+
+ it 'includes helm dependency fields' do
+ expected_fields = %w[
+ name version repository condition tags enabled import_values alias
+ ]
+
+ expect(described_class).to include_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/packages/helm/file_metadatum_type_spec.rb b/spec/graphql/types/packages/helm/file_metadatum_type_spec.rb
new file mode 100644
index 00000000000..b7bcd6213b4
--- /dev/null
+++ b/spec/graphql/types/packages/helm/file_metadatum_type_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['HelmFileMetadata'] do
+ it { expect(described_class.graphql_name).to eq('HelmFileMetadata') }
+
+ it 'includes helm file metadatum fields' do
+ expected_fields = %w[
+ created_at updated_at channel metadata
+ ]
+
+ expect(described_class).to include_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/packages/helm/maintainer_type_spec.rb b/spec/graphql/types/packages/helm/maintainer_type_spec.rb
new file mode 100644
index 00000000000..9ad51427d42
--- /dev/null
+++ b/spec/graphql/types/packages/helm/maintainer_type_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['PackageHelmMaintainerType'] do
+ it { expect(described_class.graphql_name).to eq('PackageHelmMaintainerType') }
+
+ it 'includes helm maintainer fields' do
+ expected_fields = %w[
+ name email url
+ ]
+
+ expect(described_class).to include_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/packages/helm/metadata_type_spec.rb b/spec/graphql/types/packages/helm/metadata_type_spec.rb
new file mode 100644
index 00000000000..04639450d9a
--- /dev/null
+++ b/spec/graphql/types/packages/helm/metadata_type_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['PackageHelmMetadataType'] do
+ it { expect(described_class.graphql_name).to eq('PackageHelmMetadataType') }
+
+ it 'includes helm json fields' do
+ expected_fields = %w[
+ name home sources version description keywords maintainers icon apiVersion condition tags appVersion deprecated annotations kubeVersion dependencies type
+ ]
+
+ 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 45a718683be..4f205e861dd 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe GitlabSchema.types['Project'] do
container_repositories container_repositories_count
pipeline_analytics squash_read_only sast_ci_configuration
cluster_agent cluster_agents agent_configurations
- ci_template timelogs
+ ci_template timelogs merge_commit_template
]
expect(described_class).to include_graphql_fields(*expected_fields)
@@ -296,6 +296,8 @@ RSpec.describe GitlabSchema.types['Project'] do
:last,
:merged_after,
:merged_before,
+ :created_after,
+ :created_before,
:author_username,
:assignee_username,
:reviewer_username,
diff --git a/spec/graphql/types/projects/topic_type_spec.rb b/spec/graphql/types/projects/topic_type_spec.rb
new file mode 100644
index 00000000000..01c19e111be
--- /dev/null
+++ b/spec/graphql/types/projects/topic_type_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Projects::TopicType do
+ specify { expect(described_class.graphql_name).to eq('Topic') }
+
+ specify do
+ expect(described_class).to have_graphql_fields(
+ :id,
+ :name,
+ :description,
+ :description_html,
+ :avatar_url
+ )
+ end
+end
diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb
index 14ef03a64f9..49f0980bd08 100644
--- a/spec/graphql/types/query_type_spec.rb
+++ b/spec/graphql/types/query_type_spec.rb
@@ -28,6 +28,7 @@ RSpec.describe GitlabSchema.types['Query'] do
runners
timelogs
board_list
+ topics
]
expect(described_class).to have_graphql_fields(*expected_fields).at_least
diff --git a/spec/graphql/types/release_links_type_spec.rb b/spec/graphql/types/release_links_type_spec.rb
index 38c38d58baa..e77c4e3ddd1 100644
--- a/spec/graphql/types/release_links_type_spec.rb
+++ b/spec/graphql/types/release_links_type_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe GitlabSchema.types['ReleaseLinks'] do
- it { expect(described_class).to require_graphql_authorizations(:download_code) }
+ it { expect(described_class).to require_graphql_authorizations(:read_release) }
it 'has the expected fields' do
expected_fields = %w[
@@ -18,4 +18,46 @@ RSpec.describe GitlabSchema.types['ReleaseLinks'] do
expect(described_class).to include_graphql_fields(*expected_fields)
end
+
+ context 'individual field authorization' do
+ def fetch_authorizations(field_name)
+ described_class.fields.dig(field_name).instance_variable_get(:@authorize)
+ end
+
+ describe 'openedMergeRequestsUrl' do
+ it 'has valid authorization' do
+ expect(fetch_authorizations('openedMergeRequestsUrl')).to include(:download_code)
+ end
+ end
+
+ describe 'mergedMergeRequestsUrl' do
+ it 'has valid authorization' do
+ expect(fetch_authorizations('mergedMergeRequestsUrl')).to include(:download_code)
+ end
+ end
+
+ describe 'closedMergeRequestsUrl' do
+ it 'has valid authorization' do
+ expect(fetch_authorizations('closedMergeRequestsUrl')).to include(:download_code)
+ end
+ end
+
+ describe 'openedIssuesUrl' do
+ it 'has valid authorization' do
+ expect(fetch_authorizations('openedIssuesUrl')).to include(:download_code)
+ end
+ end
+
+ describe 'closedIssuesUrl' do
+ it 'has valid authorization' do
+ expect(fetch_authorizations('closedIssuesUrl')).to include(:download_code)
+ end
+ end
+
+ describe 'editUrl' do
+ it 'has valid authorization' do
+ expect(fetch_authorizations('editUrl')).to include(:update_release)
+ end
+ end
+ end
end
diff --git a/spec/graphql/types/repository/blob_type_spec.rb b/spec/graphql/types/repository/blob_type_spec.rb
index beab4dcebc2..7f37237f355 100644
--- a/spec/graphql/types/repository/blob_type_spec.rb
+++ b/spec/graphql/types/repository/blob_type_spec.rb
@@ -23,6 +23,7 @@ RSpec.describe Types::Repository::BlobType do
:stored_externally,
:raw_path,
:replace_path,
+ :pipeline_editor_path,
:simple_viewer,
:rich_viewer,
:plain_data,
diff --git a/spec/graphql/types/user_merge_request_interaction_type_spec.rb b/spec/graphql/types/user_merge_request_interaction_type_spec.rb
index f424c9200ab..1eaaa0c23d0 100644
--- a/spec/graphql/types/user_merge_request_interaction_type_spec.rb
+++ b/spec/graphql/types/user_merge_request_interaction_type_spec.rb
@@ -78,7 +78,7 @@ RSpec.describe GitlabSchema.types['UserMergeRequestInteraction'] do
merge_request.reviewers << user
end
- it { is_expected.to eq(Types::MergeRequestReviewStateEnum.values['UNREVIEWED'].value) }
+ it { is_expected.to eq(Types::MergeRequestReviewStateEnum.values['ATTENTION_REQUESTED'].value) }
it 'implies not reviewed' do
expect(resolve(:reviewed)).to be false
@@ -87,7 +87,8 @@ RSpec.describe GitlabSchema.types['UserMergeRequestInteraction'] do
context 'when the user has provided a review' do
before do
- merge_request.merge_request_reviewers.create!(reviewer: user, state: MergeRequestReviewer.states['reviewed'])
+ reviewer = merge_request.merge_request_reviewers.create!(reviewer: user)
+ reviewer.update!(state: MergeRequestReviewer.states['reviewed'])
end
it { is_expected.to eq(Types::MergeRequestReviewStateEnum.values['REVIEWED'].value) }
diff --git a/spec/helpers/admin/deploy_key_helper_spec.rb b/spec/helpers/admin/deploy_key_helper_spec.rb
new file mode 100644
index 00000000000..ca951ccf485
--- /dev/null
+++ b/spec/helpers/admin/deploy_key_helper_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Admin::DeployKeyHelper do
+ describe '#admin_deploy_keys_data' do
+ let_it_be(:edit_path) { '/admin/deploy_keys/:id/edit' }
+ let_it_be(:delete_path) { '/admin/deploy_keys/:id' }
+ let_it_be(:create_path) { '/admin/deploy_keys/new' }
+ let_it_be(:empty_state_svg_path) { '/assets/illustrations/empty-state/empty-deploy-keys-lg.svg' }
+
+ subject(:result) { helper.admin_deploy_keys_data }
+
+ it 'returns correct hash' do
+ expect(helper).to receive(:edit_admin_deploy_key_path).with(':id').and_return(edit_path)
+ expect(helper).to receive(:admin_deploy_key_path).with(':id').and_return(delete_path)
+ expect(helper).to receive(:new_admin_deploy_key_path).and_return(create_path)
+ expect(helper).to receive(:image_path).with('illustrations/empty-state/empty-deploy-keys-lg.svg').and_return(empty_state_svg_path)
+
+ expect(result).to eq({
+ edit_path: edit_path,
+ delete_path: delete_path,
+ create_path: create_path,
+ empty_state_svg_path: empty_state_svg_path
+ })
+ end
+ end
+end
diff --git a/spec/helpers/boards_helper_spec.rb b/spec/helpers/boards_helper_spec.rb
index cb4b6915b20..ec949fde30e 100644
--- a/spec/helpers/boards_helper_spec.rb
+++ b/spec/helpers/boards_helper_spec.rb
@@ -23,14 +23,14 @@ RSpec.describe BoardsHelper do
it 'returns correct path for base group' do
assign(:board, group_board)
- expect(helper.build_issue_link_base).to eq('/base/:project_path/issues')
+ expect(helper.build_issue_link_base).to eq('/:project_path/-/issues')
end
it 'returns correct path for subgroup' do
subgroup = create(:group, parent: base_group, path: 'sub')
assign(:board, create(:board, group: subgroup))
- expect(helper.build_issue_link_base).to eq('/base/sub/:project_path/issues')
+ expect(helper.build_issue_link_base).to eq('/:project_path/-/issues')
end
end
end
@@ -149,7 +149,7 @@ RSpec.describe BoardsHelper do
end
it 'returns correct path for base group' do
- expect(helper.build_issue_link_base).to eq("/#{base_group.full_path}/:project_path/issues")
+ expect(helper.build_issue_link_base).to eq("/:project_path/-/issues")
end
it 'returns required label endpoints' do
diff --git a/spec/helpers/ci/pipelines_helper_spec.rb b/spec/helpers/ci/pipelines_helper_spec.rb
index 94b5e707d73..751bcc97582 100644
--- a/spec/helpers/ci/pipelines_helper_spec.rb
+++ b/spec/helpers/ci/pipelines_helper_spec.rb
@@ -71,4 +71,26 @@ RSpec.describe Ci::PipelinesHelper do
it { expect(has_gitlab_ci?).to eq(result) }
end
end
+
+ describe 'has_pipeline_badges?' do
+ let(:pipeline) { create(:ci_empty_pipeline) }
+
+ subject { helper.has_pipeline_badges?(pipeline) }
+
+ context 'when pipeline has a badge' do
+ before do
+ pipeline.drop!(:config_error)
+ end
+
+ it 'shows pipeline badges' do
+ expect(subject).to eq(true)
+ end
+ end
+
+ context 'when pipeline has no badges' do
+ it 'shows pipeline badges' do
+ expect(subject).to eq(false)
+ end
+ end
+ end
end
diff --git a/spec/helpers/ci/runners_helper_spec.rb b/spec/helpers/ci/runners_helper_spec.rb
index 49ea2ac8d3b..173a0d3ab3c 100644
--- a/spec/helpers/ci/runners_helper_spec.rb
+++ b/spec/helpers/ci/runners_helper_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Ci::RunnersHelper do
- let_it_be(:user, refind: true) { create(:user) }
+ let_it_be(:user) { create(:user) }
before do
allow(helper).to receive(:current_user).and_return(user)
@@ -12,22 +12,22 @@ RSpec.describe Ci::RunnersHelper do
describe '#runner_status_icon', :clean_gitlab_redis_cache do
it "returns - not contacted yet" do
runner = create(:ci_runner)
- expect(runner_status_icon(runner)).to include("not connected yet")
+ expect(helper.runner_status_icon(runner)).to include("not connected yet")
end
it "returns offline text" do
runner = create(:ci_runner, contacted_at: 1.day.ago, active: true)
- expect(runner_status_icon(runner)).to include("Runner is offline")
+ expect(helper.runner_status_icon(runner)).to include("Runner is offline")
end
it "returns online text" do
runner = create(:ci_runner, contacted_at: 1.second.ago, active: true)
- expect(runner_status_icon(runner)).to include("Runner is online")
+ expect(helper.runner_status_icon(runner)).to include("Runner is online")
end
it "returns paused text" do
runner = create(:ci_runner, contacted_at: 1.second.ago, active: false)
- expect(runner_status_icon(runner)).to include("Runner is paused")
+ expect(helper.runner_status_icon(runner)).to include("Runner is paused")
end
end
@@ -42,7 +42,7 @@ RSpec.describe Ci::RunnersHelper do
context 'without sorting' do
it 'returns cached value' do
- expect(runner_contacted_at(runner)).to eq(contacted_at_cached)
+ expect(helper.runner_contacted_at(runner)).to eq(contacted_at_cached)
end
end
@@ -52,7 +52,7 @@ RSpec.describe Ci::RunnersHelper do
end
it 'returns cached value' do
- expect(runner_contacted_at(runner)).to eq(contacted_at_cached)
+ expect(helper.runner_contacted_at(runner)).to eq(contacted_at_cached)
end
end
@@ -62,29 +62,63 @@ RSpec.describe Ci::RunnersHelper do
end
it 'returns stored value' do
- expect(runner_contacted_at(runner)).to eq(contacted_at_stored)
+ expect(helper.runner_contacted_at(runner)).to eq(contacted_at_stored)
end
end
end
+ describe '#admin_runners_data_attributes' do
+ let_it_be(:admin) { create(:user, :admin) }
+ let_it_be(:instance_runner) { create(:ci_runner, :instance) }
+ let_it_be(:project_runner) { create(:ci_runner, :project ) }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(admin)
+ end
+
+ it 'returns the data in format' do
+ expect(helper.admin_runners_data_attributes).to eq({
+ runner_install_help_page: 'https://docs.gitlab.com/runner/install/',
+ registration_token: Gitlab::CurrentSettings.runners_registration_token,
+ active_runners_count: '0',
+ all_runners_count: '2',
+ instance_runners_count: '1',
+ group_runners_count: '0',
+ project_runners_count: '1'
+ })
+ end
+ end
+
describe '#group_shared_runners_settings_data' do
- let(:group) { create(:group, parent: parent, shared_runners_enabled: false) }
- let(:parent) { create(:group) }
+ let_it_be(:parent) { create(:group) }
+ let_it_be(:group) { create(:group, parent: parent, shared_runners_enabled: false) }
+
+ let(:runner_constants) do
+ {
+ runner_enabled: Namespace::SR_ENABLED,
+ runner_disabled: Namespace::SR_DISABLED_AND_UNOVERRIDABLE,
+ runner_allow_override: Namespace::SR_DISABLED_WITH_OVERRIDE
+ }
+ end
it 'returns group data for top level group' do
- data = group_shared_runners_settings_data(parent)
+ result = {
+ update_path: "/api/v4/groups/#{parent.id}",
+ shared_runners_availability: Namespace::SR_ENABLED,
+ parent_shared_runners_availability: nil
+ }.merge(runner_constants)
- expect(data[:update_path]).to eq("/api/v4/groups/#{parent.id}")
- expect(data[:shared_runners_availability]).to eq('enabled')
- expect(data[:parent_shared_runners_availability]).to eq(nil)
+ expect(helper.group_shared_runners_settings_data(parent)).to eq result
end
it 'returns group data for child group' do
- data = group_shared_runners_settings_data(group)
+ result = {
+ update_path: "/api/v4/groups/#{group.id}",
+ shared_runners_availability: Namespace::SR_DISABLED_AND_UNOVERRIDABLE,
+ parent_shared_runners_availability: Namespace::SR_ENABLED
+ }.merge(runner_constants)
- expect(data[:update_path]).to eq("/api/v4/groups/#{group.id}")
- expect(data[:shared_runners_availability]).to eq(Namespace::SR_DISABLED_AND_UNOVERRIDABLE)
- expect(data[:parent_shared_runners_availability]).to eq('enabled')
+ expect(helper.group_shared_runners_settings_data(group)).to eq result
end
end
@@ -92,7 +126,7 @@ RSpec.describe Ci::RunnersHelper do
let(:group) { create(:group) }
it 'returns group data to render a runner list' do
- data = group_runners_data_attributes(group)
+ data = helper.group_runners_data_attributes(group)
expect(data[:registration_token]).to eq(group.runners_token)
expect(data[:group_id]).to eq(group.id)
diff --git a/spec/helpers/clusters_helper_spec.rb b/spec/helpers/clusters_helper_spec.rb
index f1e19f17c72..51f111917d1 100644
--- a/spec/helpers/clusters_helper_spec.rb
+++ b/spec/helpers/clusters_helper_spec.rb
@@ -59,54 +59,96 @@ RSpec.describe ClustersHelper do
end
end
- describe '#js_cluster_agents_list_data' do
- let_it_be(:project) { build(:project, :repository) }
+ describe '#js_clusters_list_data' do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project) { build(:project) }
+ let_it_be(:clusterable) { ClusterablePresenter.fabricate(project, current_user: current_user) }
- subject { helper.js_cluster_agents_list_data(project) }
+ subject { helper.js_clusters_list_data(clusterable) }
- it 'displays project default branch' do
- expect(subject[:default_branch_name]).to eq(project.default_branch)
+ it 'displays endpoint path' do
+ expect(subject[:endpoint]).to eq("#{project_path(project)}/-/clusters.json")
end
- it 'displays image path' do
- expect(subject[:empty_state_image]).to match(%r(/illustrations/logos/clusters_empty|svg))
+ it 'generates svg image data', :aggregate_failures do
+ expect(subject.dig(:img_tags, :aws, :path)).to match(%r(/illustrations/logos/amazon_eks|svg))
+ expect(subject.dig(:img_tags, :default, :path)).to match(%r(/illustrations/logos/kubernetes|svg))
+ expect(subject.dig(:img_tags, :gcp, :path)).to match(%r(/illustrations/logos/google_gke|svg))
+
+ expect(subject.dig(:img_tags, :aws, :text)).to eq('Amazon EKS')
+ expect(subject.dig(:img_tags, :default, :text)).to eq('Kubernetes Cluster')
+ expect(subject.dig(:img_tags, :gcp, :text)).to eq('Google GKE')
end
- it 'displays project path' do
- expect(subject[:project_path]).to eq(project.full_path)
+ it 'displays and ancestor_help_path' do
+ expect(subject[:ancestor_help_path]).to eq(help_page_path('user/group/clusters/index', anchor: 'cluster-precedence'))
end
- it 'generates docs urls' do
- expect(subject[:agent_docs_url]).to eq(help_page_path('user/clusters/agent/index'))
- expect(subject[:install_docs_url]).to eq(help_page_path('administration/clusters/kas'))
- expect(subject[:get_started_docs_url]).to eq(help_page_path('user/clusters/agent/index', anchor: 'define-a-configuration-repository'))
- expect(subject[:integration_docs_url]).to eq(help_page_path('user/clusters/agent/index', anchor: 'get-started-with-gitops-and-the-gitlab-agent'))
+ it 'displays empty image path' do
+ expect(subject[:clusters_empty_state_image]).to match(%r(/illustrations/empty-state/empty-state-clusters|svg))
end
- it 'displays kas address' do
- expect(subject[:kas_address]).to eq(Gitlab::Kas.external_url)
+ it 'displays create cluster using certificate path' do
+ expect(subject[:new_cluster_path]).to eq("#{project_path(project)}/-/clusters/new?tab=create")
+ end
+
+ context 'user has no permissions to create a cluster' do
+ it 'displays that user can\t add cluster' do
+ expect(subject[:can_add_cluster]).to eq("false")
+ end
+ end
+
+ context 'user is a maintainer' do
+ before do
+ project.add_maintainer(current_user)
+ end
+
+ it 'displays that the user can add cluster' do
+ expect(subject[:can_add_cluster]).to eq("true")
+ end
+ end
+
+ context 'project cluster' do
+ it 'doesn\'t display empty state help text' do
+ expect(subject[:empty_state_help_text]).to be_nil
+ end
+ end
+
+ context 'group cluster' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:clusterable) { ClusterablePresenter.fabricate(group, current_user: current_user) }
+
+ it 'displays empty state help text' do
+ expect(subject[:empty_state_help_text]).to eq(s_('ClusterIntegration|Adding an integration to your group will share the cluster across all your projects.'))
+ end
end
end
- describe '#js_clusters_list_data' do
- subject { helper.js_clusters_list_data('/path') }
+ describe '#js_clusters_data' do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project) { build(:project) }
+ let_it_be(:clusterable) { ClusterablePresenter.fabricate(project, current_user: current_user) }
- it 'displays endpoint path' do
- expect(subject[:endpoint]).to eq('/path')
+ subject { helper.js_clusters_data(clusterable) }
+
+ it 'displays project default branch' do
+ expect(subject[:default_branch_name]).to eq(project.default_branch)
end
- it 'generates svg image data', :aggregate_failures do
- expect(subject.dig(:img_tags, :aws, :path)).to match(%r(/illustrations/logos/amazon_eks|svg))
- expect(subject.dig(:img_tags, :default, :path)).to match(%r(/illustrations/logos/kubernetes|svg))
- expect(subject.dig(:img_tags, :gcp, :path)).to match(%r(/illustrations/logos/google_gke|svg))
+ it 'displays image path' do
+ expect(subject[:empty_state_image]).to match(%r(/illustrations/empty-state/empty-state-agents|svg))
+ end
- expect(subject.dig(:img_tags, :aws, :text)).to eq('Amazon EKS')
- expect(subject.dig(:img_tags, :default, :text)).to eq('Kubernetes Cluster')
- expect(subject.dig(:img_tags, :gcp, :text)).to eq('Google GKE')
+ it 'displays project path' do
+ expect(subject[:project_path]).to eq(project.full_path)
end
- it 'displays and ancestor_help_path' do
- expect(subject[:ancestor_help_path]).to eq(help_page_path('user/group/clusters/index', anchor: 'cluster-precedence'))
+ it 'displays add cluster using certificate path' do
+ expect(subject[:add_cluster_path]).to eq("#{project_path(project)}/-/clusters/new?tab=add")
+ end
+
+ it 'displays kas address' do
+ expect(subject[:kas_address]).to eq(Gitlab::Kas.external_url)
end
end
@@ -152,4 +194,24 @@ RSpec.describe ClustersHelper do
end
end
end
+
+ describe '#display_cluster_agents?' do
+ subject { helper.display_cluster_agents?(clusterable) }
+
+ context 'when clusterable is a project' do
+ let(:clusterable) { build(:project) }
+
+ it 'allows agents to display' do
+ expect(subject).to be_truthy
+ end
+ end
+
+ context 'when clusterable is a group' do
+ let(:clusterable) { build(:group) }
+
+ it 'does not allow agents to display' do
+ expect(subject).to be_falsey
+ end
+ end
+ end
end
diff --git a/spec/helpers/emoji_helper_spec.rb b/spec/helpers/emoji_helper_spec.rb
index 15e4ce03960..6f4c962c0fb 100644
--- a/spec/helpers/emoji_helper_spec.rb
+++ b/spec/helpers/emoji_helper_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe EmojiHelper do
describe '#emoji_icon' do
let(:options) { {} }
let(:emoji_text) { 'rocket' }
+ let(:unicode_version) { '6.0' }
let(:aria_hidden_option) { "aria-hidden=\"true\"" }
subject { helper.emoji_icon(emoji_text, options) }
@@ -14,7 +15,7 @@ RSpec.describe EmojiHelper do
is_expected.to include('<gl-emoji',
"title=\"#{emoji_text}\"",
"data-name=\"#{emoji_text}\"",
- "data-unicode-version=\"#{::Gitlab::Emoji.emoji_unicode_version(emoji_text)}\"")
+ "data-unicode-version=\"#{unicode_version}\"")
is_expected.not_to include(aria_hidden_option)
end
@@ -25,7 +26,7 @@ RSpec.describe EmojiHelper do
is_expected.to include('<gl-emoji',
"title=\"#{emoji_text}\"",
"data-name=\"#{emoji_text}\"",
- "data-unicode-version=\"#{::Gitlab::Emoji.emoji_unicode_version(emoji_text)}\"",
+ "data-unicode-version=\"#{unicode_version}\"",
aria_hidden_option)
end
end
diff --git a/spec/helpers/environments_helper_spec.rb b/spec/helpers/environments_helper_spec.rb
index 60bed247d85..aef240db5b8 100644
--- a/spec/helpers/environments_helper_spec.rb
+++ b/spec/helpers/environments_helper_spec.rb
@@ -40,12 +40,10 @@ RSpec.describe EnvironmentsHelper do
'validate_query_path' => validate_query_project_prometheus_metrics_path(project),
'custom_metrics_available' => 'true',
'alerts_endpoint' => project_prometheus_alerts_path(project, environment_id: environment.id, format: :json),
- 'prometheus_alerts_available' => 'true',
'custom_dashboard_base_path' => Gitlab::Metrics::Dashboard::RepoDashboardFinder::DASHBOARD_ROOT,
'operations_settings_path' => project_settings_operations_path(project),
'can_access_operations_settings' => 'true',
- 'panel_preview_endpoint' => project_metrics_dashboards_builder_path(project, format: :json),
- 'has_managed_prometheus' => 'false'
+ 'panel_preview_endpoint' => project_metrics_dashboards_builder_path(project, format: :json)
)
end
@@ -63,20 +61,6 @@ RSpec.describe EnvironmentsHelper do
end
end
- context 'without read_prometheus_alerts permission' do
- before do
- allow(helper).to receive(:can?)
- .with(user, :read_prometheus_alerts, project)
- .and_return(false)
- end
-
- it 'returns false' do
- expect(metrics_data).to include(
- 'prometheus_alerts_available' => 'false'
- )
- end
- end
-
context 'with metrics_setting' do
before do
create(:project_metrics_setting, project: project, external_dashboard_url: 'http://gitlab.com')
@@ -120,52 +104,6 @@ RSpec.describe EnvironmentsHelper do
end
end
end
-
- context 'has_managed_prometheus' do
- context 'without prometheus integration' do
- it "doesn't have managed prometheus" do
- expect(metrics_data).to include(
- 'has_managed_prometheus' => 'false'
- )
- end
- end
-
- context 'with prometheus integration' do
- let_it_be(:prometheus_integration) { create(:prometheus_integration, project: project) }
-
- context 'when manual prometheus integration is active' do
- it "doesn't have managed prometheus" do
- prometheus_integration.update!(manual_configuration: true)
-
- expect(metrics_data).to include(
- 'has_managed_prometheus' => 'false'
- )
- end
- end
-
- context 'when prometheus integration is inactive' do
- it "doesn't have managed prometheus" do
- prometheus_integration.update!(manual_configuration: false)
-
- expect(metrics_data).to include(
- 'has_managed_prometheus' => 'false'
- )
- end
- end
-
- context 'when a cluster prometheus is available' do
- let(:cluster) { create(:cluster, projects: [project]) }
-
- it 'has managed prometheus' do
- create(:clusters_integrations_prometheus, cluster: cluster)
-
- expect(metrics_data).to include(
- 'has_managed_prometheus' => 'true'
- )
- end
- end
- end
- end
end
describe '#custom_metrics_available?' do
diff --git a/spec/helpers/graph_helper_spec.rb b/spec/helpers/graph_helper_spec.rb
index 0930417accb..a5d4e1313e1 100644
--- a/spec/helpers/graph_helper_spec.rb
+++ b/spec/helpers/graph_helper_spec.rb
@@ -27,4 +27,16 @@ RSpec.describe GraphHelper do
expect(should_render_dora_charts).to be(false)
end
end
+
+ describe '#should_render_quality_summary' do
+ let(:project) { create(:project, :private) }
+
+ before do
+ self.instance_variable_set(:@project, project)
+ end
+
+ it 'always returns false' do
+ expect(should_render_quality_summary).to be(false)
+ end
+ end
end
diff --git a/spec/helpers/groups/settings_helper_spec.rb b/spec/helpers/groups/settings_helper_spec.rb
new file mode 100644
index 00000000000..f8c0bfc19a1
--- /dev/null
+++ b/spec/helpers/groups/settings_helper_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Groups::SettingsHelper do
+ include GroupsHelper
+
+ let_it_be(:group) { create(:group, path: "foo") }
+
+ describe('#group_settings_confirm_modal_data') do
+ using RSpec::Parameterized::TableSyntax
+
+ fake_form_id = "fake_form_id"
+
+ where(:is_paid, :is_button_disabled, :form_value_id) do
+ true | "true" | nil
+ true | "true" | fake_form_id
+ false | "false" | nil
+ false | "false" | fake_form_id
+ end
+
+ with_them do
+ it "returns expected parameters" do
+ allow(group).to receive(:paid?).and_return(is_paid)
+
+ expected = helper.group_settings_confirm_modal_data(group, form_value_id)
+ expect(expected).to eq({
+ button_text: "Remove group",
+ confirm_danger_message: remove_group_message(group),
+ remove_form_id: form_value_id,
+ phrase: group.full_path,
+ button_testid: "remove-group-button",
+ disabled: is_button_disabled
+ })
+ end
+ end
+ end
+end
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 4d647696130..8859ed27022 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -92,7 +92,7 @@ RSpec.describe GroupsHelper do
shared_examples 'correct ancestor order' do
it 'outputs the groups in the correct order' do
expect(subject)
- .to match(%r{<li style="text-indent: 16px;"><a.*>#{deep_nested_group.name}.*</li>.*<a.*>#{very_deep_nested_group.name}</a>}m)
+ .to match(%r{<li><a.*>#{deep_nested_group.name}.*</li>.*<a.*>#{very_deep_nested_group.name}</a>}m)
end
end
diff --git a/spec/helpers/invite_members_helper_spec.rb b/spec/helpers/invite_members_helper_spec.rb
index e0e05140d6c..02f0416a17a 100644
--- a/spec/helpers/invite_members_helper_spec.rb
+++ b/spec/helpers/invite_members_helper_spec.rb
@@ -59,7 +59,84 @@ RSpec.describe InviteMembersHelper do
no_selection_areas_of_focus: []
}
- expect(helper.common_invite_modal_dataset(project)).to match(attributes)
+ expect(helper.common_invite_modal_dataset(project)).to include(attributes)
+ end
+ end
+
+ context 'tasks_to_be_done' do
+ subject(:output) { helper.common_invite_modal_dataset(source) }
+
+ let_it_be(:source) { project }
+
+ before do
+ stub_experiments(invite_members_for_task: true)
+ end
+
+ context 'when not logged in' do
+ before do
+ allow(helper).to receive(:params).and_return({ open_modal: 'invite_members_for_task' })
+ end
+
+ it "doesn't have the tasks to be done attributes" do
+ expect(output[:tasks_to_be_done_options]).to be_nil
+ expect(output[:projects]).to be_nil
+ expect(output[:new_project_path]).to be_nil
+ end
+ end
+
+ context 'when logged in but the open_modal param is not present' do
+ before do
+ allow(helper).to receive(:current_user).and_return(developer)
+ end
+
+ it "doesn't have the tasks to be done attributes" do
+ expect(output[:tasks_to_be_done_options]).to be_nil
+ expect(output[:projects]).to be_nil
+ expect(output[:new_project_path]).to be_nil
+ end
+ end
+
+ context 'when logged in and the open_modal param is present' do
+ before do
+ allow(helper).to receive(:current_user).and_return(developer)
+ allow(helper).to receive(:params).and_return({ open_modal: 'invite_members_for_task' })
+ end
+
+ context 'for a group' do
+ let_it_be(:source) { create(:group, projects: [project]) }
+
+ it 'has the expected attributes', :aggregate_failures do
+ expect(output[:tasks_to_be_done_options]).to eq(
+ [
+ { value: :code, text: 'Create/import code into a project (repository)' },
+ { value: :ci, text: 'Set up CI/CD pipelines to build, test, deploy, and monitor code' },
+ { value: :issues, text: 'Create/import issues (tickets) to collaborate on ideas and plan work' }
+ ].to_json
+ )
+ expect(output[:projects]).to eq(
+ [{ id: project.id, title: project.title }].to_json
+ )
+ expect(output[:new_project_path]).to eq(
+ new_project_path(namespace_id: source.id)
+ )
+ end
+ end
+
+ context 'for a project' do
+ it 'has the expected attributes', :aggregate_failures do
+ expect(output[:tasks_to_be_done_options]).to eq(
+ [
+ { value: :code, text: 'Create/import code into a project (repository)' },
+ { value: :ci, text: 'Set up CI/CD pipelines to build, test, deploy, and monitor code' },
+ { value: :issues, text: 'Create/import issues (tickets) to collaborate on ideas and plan work' }
+ ].to_json
+ )
+ expect(output[:projects]).to eq(
+ [{ id: project.id, title: project.title }].to_json
+ )
+ expect(output[:new_project_path]).to eq('')
+ end
+ end
end
end
end
diff --git a/spec/helpers/issuables_description_templates_helper_spec.rb b/spec/helpers/issuables_description_templates_helper_spec.rb
index 55649e9087a..6b05bab7432 100644
--- a/spec/helpers/issuables_description_templates_helper_spec.rb
+++ b/spec/helpers/issuables_description_templates_helper_spec.rb
@@ -44,7 +44,7 @@ RSpec.describe IssuablesDescriptionTemplatesHelper, :clean_gitlab_redis_cache do
end
end
- describe '#issuable_templates_names' do
+ describe '#selected_template' do
let_it_be(:project) { build(:project) }
before do
@@ -63,7 +63,14 @@ RSpec.describe IssuablesDescriptionTemplatesHelper, :clean_gitlab_redis_cache do
end
it 'returns project templates' do
- expect(helper.issuable_templates_names(Issue.new)).to eq(%w[another_issue_template custom_issue_template])
+ value = [
+ "",
+ [
+ { name: "another_issue_template", id: "another_issue_template", project_id: project.id },
+ { name: "custom_issue_template", id: "custom_issue_template", project_id: project.id }
+ ]
+ ].to_json
+ expect(helper.available_service_desk_templates_for(@project)).to eq(value)
end
end
@@ -71,7 +78,8 @@ RSpec.describe IssuablesDescriptionTemplatesHelper, :clean_gitlab_redis_cache do
let(:templates) { {} }
it 'returns empty array' do
- expect(helper.issuable_templates_names(Issue.new)).to eq([])
+ value = [].to_json
+ expect(helper.available_service_desk_templates_for(@project)).to eq(value)
end
end
end
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index 30049745433..fa19395ebc7 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -169,26 +169,9 @@ RSpec.describe IssuablesHelper do
stub_const("Gitlab::IssuablesCountForState::THRESHOLD", 1000)
end
- context 'when feature flag cached_issues_state_count is disabled' do
- before do
- stub_feature_flags(cached_issues_state_count: false)
- end
-
- it 'returns complete count' do
- expect(helper.issuables_state_counter_text(:issues, :opened, true))
- .to eq('<span>Open</span> <span class="badge badge-muted badge-pill gl-badge gl-tab-counter-badge sm gl-display-none gl-sm-display-inline-flex">1,100</span>')
- end
- end
-
- context 'when feature flag cached_issues_state_count is enabled' do
- before do
- stub_feature_flags(cached_issues_state_count: true)
- end
-
- it 'returns truncated count' do
- expect(helper.issuables_state_counter_text(:issues, :opened, true))
- .to eq('<span>Open</span> <span class="badge badge-muted badge-pill gl-badge gl-tab-counter-badge sm gl-display-none gl-sm-display-inline-flex">1.1k</span>')
- end
+ it 'returns truncated count' do
+ expect(helper.issuables_state_counter_text(:issues, :opened, true))
+ .to eq('<span>Open</span> <span class="badge badge-muted badge-pill gl-badge gl-tab-counter-badge sm gl-display-none gl-sm-display-inline-flex">1.1k</span>')
end
end
end
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index 850051c7875..43b27dded3b 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -326,6 +326,7 @@ RSpec.describe IssuesHelper do
new_issue_path: new_project_issue_path(project, issue: { milestone_id: finder.milestones.first.id }),
project_import_jira_path: project_import_jira_path(project),
quick_actions_help_path: help_page_path('user/project/quick_actions'),
+ releases_path: project_releases_path(project, format: :json),
reset_path: new_issuable_address_project_path(project, issuable_type: 'issue'),
rss_path: '#',
show_new_issue_link: 'true',
diff --git a/spec/helpers/learn_gitlab_helper_spec.rb b/spec/helpers/learn_gitlab_helper_spec.rb
index 1159fd96d59..b9f34853a77 100644
--- a/spec/helpers/learn_gitlab_helper_spec.rb
+++ b/spec/helpers/learn_gitlab_helper_spec.rb
@@ -11,9 +11,6 @@ RSpec.describe LearnGitlabHelper do
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::Project) do |learn_gitlab|
allow(learn_gitlab).to receive(:project).and_return(project)
end
@@ -22,38 +19,7 @@ RSpec.describe LearnGitlabHelper do
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(
- :issue_created,
- :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, LearnGitlab::Onboarding::ACTION_ISSUE_IDS[:git_write]),
- completed: true,
- svg: helper.image_path("learn_gitlab/git_write.svg")
- })
- expect(onboarding_actions_data[:pipeline_created]).to eq({
- url: project_issue_url(project, LearnGitlab::Onboarding::ACTION_ISSUE_IDS[:pipeline_created]),
- completed: false,
- svg: helper.image_path("learn_gitlab/pipeline_created.svg")
- })
- end
- end
-
- describe '.learn_gitlab_enabled?' do
+ describe '#learn_gitlab_enabled?' do
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) }
@@ -89,14 +55,121 @@ RSpec.describe LearnGitlabHelper do
end
end
- describe '.onboarding_sections_data' do
- subject(:sections) { helper.onboarding_sections_data }
+ describe '#learn_gitlab_data' do
+ subject(:learn_gitlab_data) { helper.learn_gitlab_data(project) }
+
+ let(:onboarding_actions_data) { Gitlab::Json.parse(learn_gitlab_data[:actions]).deep_symbolize_keys }
+ let(:onboarding_sections_data) { Gitlab::Json.parse(learn_gitlab_data[:sections]).deep_symbolize_keys }
+
+ shared_examples 'has all data' do
+ it 'has all actions' do
+ expected_keys = [
+ :issue_created,
+ :git_write,
+ :pipeline_created,
+ :merge_request_created,
+ :user_added,
+ :trial_started,
+ :required_mr_approvals_enabled,
+ :code_owners_enabled,
+ :security_scan_enabled
+ ]
+
+ expect(onboarding_actions_data.keys).to contain_exactly(*expected_keys)
+ end
- it 'has the right keys' do
- expect(sections.keys).to contain_exactly(:deploy, :plan, :workspace)
+ it 'has all section data', :aggregate_failures do
+ expect(onboarding_sections_data.keys).to contain_exactly(:deploy, :plan, :workspace)
+ expect(onboarding_sections_data.values.map { |section| section.keys }).to match_array([[:svg]] * 3)
+ end
end
- it 'has the svg' do
- expect(sections.values.map { |section| section.keys }).to eq([[:svg]] * 3)
+
+ it_behaves_like 'has all data'
+
+ it 'sets correct paths' do
+ expect(onboarding_actions_data).to match({
+ trial_started: a_hash_including(
+ url: a_string_matching(%r{/learn_gitlab/-/issues/2\z})
+ ),
+ issue_created: a_hash_including(
+ url: a_string_matching(%r{/learn_gitlab/-/issues/4\z})
+ ),
+ git_write: a_hash_including(
+ url: a_string_matching(%r{/learn_gitlab/-/issues/6\z})
+ ),
+ pipeline_created: a_hash_including(
+ url: a_string_matching(%r{/learn_gitlab/-/issues/7\z})
+ ),
+ user_added: a_hash_including(
+ url: a_string_matching(%r{/learn_gitlab/-/issues/8\z})
+ ),
+ merge_request_created: a_hash_including(
+ url: a_string_matching(%r{/learn_gitlab/-/issues/9\z})
+ ),
+ code_owners_enabled: a_hash_including(
+ url: a_string_matching(%r{/learn_gitlab/-/issues/10\z})
+ ),
+ required_mr_approvals_enabled: a_hash_including(
+ url: a_string_matching(%r{/learn_gitlab/-/issues/11\z})
+ ),
+ security_scan_enabled: a_hash_including(
+ url: a_string_matching(%r{docs\.gitlab\.com/ee/user/application_security/security_dashboard/#gitlab-security-dashboard-security-center-and-vulnerability-reports\z})
+ )
+ })
+ end
+
+ it 'sets correct completion statuses' do
+ expect(onboarding_actions_data).to match({
+ issue_created: a_hash_including(completed: false),
+ git_write: a_hash_including(completed: true),
+ pipeline_created: a_hash_including(completed: false),
+ merge_request_created: a_hash_including(completed: false),
+ user_added: a_hash_including(completed: false),
+ trial_started: a_hash_including(completed: false),
+ required_mr_approvals_enabled: a_hash_including(completed: false),
+ code_owners_enabled: a_hash_including(completed: false),
+ security_scan_enabled: a_hash_including(completed: false)
+ })
+ end
+
+ context 'when in the new action URLs experiment' do
+ before do
+ stub_experiments(change_continuous_onboarding_link_urls: :candidate)
+ end
+
+ it_behaves_like 'has all data'
+
+ it 'sets mostly new paths' do
+ expect(onboarding_actions_data).to match({
+ trial_started: a_hash_including(
+ url: a_string_matching(%r{/learn_gitlab/-/issues/2\z})
+ ),
+ issue_created: a_hash_including(
+ url: a_string_matching(%r{/learn_gitlab/-/issues\z})
+ ),
+ git_write: a_hash_including(
+ url: a_string_matching(%r{/learn_gitlab\z})
+ ),
+ pipeline_created: a_hash_including(
+ url: a_string_matching(%r{/learn_gitlab/-/pipelines\z})
+ ),
+ user_added: a_hash_including(
+ url: a_string_matching(%r{/learn_gitlab/-/project_members\z})
+ ),
+ merge_request_created: a_hash_including(
+ url: a_string_matching(%r{/learn_gitlab/-/merge_requests\z})
+ ),
+ code_owners_enabled: a_hash_including(
+ url: a_string_matching(%r{/learn_gitlab/-/issues/10\z})
+ ),
+ required_mr_approvals_enabled: a_hash_including(
+ url: a_string_matching(%r{/learn_gitlab/-/issues/11\z})
+ ),
+ security_scan_enabled: a_hash_including(
+ url: a_string_matching(%r{/learn_gitlab/-/security/configuration\z})
+ )
+ })
+ end
end
end
end
diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb
index c671379c4b4..e94eb63fc2c 100644
--- a/spec/helpers/members_helper_spec.rb
+++ b/spec/helpers/members_helper_spec.rb
@@ -68,4 +68,10 @@ RSpec.describe MembersHelper do
it { expect(leave_confirmation_message(project)).to eq "Are you sure you want to leave the \"#{project.full_name}\" project?" }
it { expect(leave_confirmation_message(group)).to eq "Are you sure you want to leave the \"#{group.name}\" group?" }
end
+
+ describe '#localized_tasks_to_be_done_choices' do
+ it 'has a translation for all `TASKS_TO_BE_DONE` keys' do
+ expect(localized_tasks_to_be_done_choices).to include(*MemberTask::TASKS.keys)
+ end
+ end
end
diff --git a/spec/helpers/nav/top_nav_helper_spec.rb b/spec/helpers/nav/top_nav_helper_spec.rb
index da7e5d5dce2..10bd45e3189 100644
--- a/spec/helpers/nav/top_nav_helper_spec.rb
+++ b/spec/helpers/nav/top_nav_helper_spec.rb
@@ -188,6 +188,11 @@ RSpec.describe Nav::TopNavHelper do
href: '/explore',
id: 'explore',
title: 'Explore projects'
+ ),
+ ::Gitlab::Nav::TopNavMenuItem.build(
+ href: '/explore/projects/topics',
+ id: 'topics',
+ title: 'Explore topics'
)
]
expect(projects_view[:linksPrimary]).to eq(expected_links_primary)
diff --git a/spec/helpers/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb
index fc62bbf8bf8..913a38d353f 100644
--- a/spec/helpers/notes_helper_spec.rb
+++ b/spec/helpers/notes_helper_spec.rb
@@ -322,11 +322,21 @@ RSpec.describe NotesHelper do
describe '#notes_data' do
let(:issue) { create(:issue, project: project) }
- it 'sets last_fetched_at to 0 when start_at_zero is true' do
+ before do
@project = project
@noteable = issue
+ allow(helper).to receive(:current_user).and_return(guest)
+ end
+
+ it 'sets last_fetched_at to 0 when start_at_zero is true' do
expect(helper.notes_data(issue, true)[:lastFetchedAt]).to eq(0)
end
+
+ it 'includes the current notes filter for the user' do
+ guest.set_notes_filter(UserPreference::NOTES_FILTERS[:only_comments], issue)
+
+ expect(helper.notes_data(issue)[:notesFilter]).to eq(UserPreference::NOTES_FILTERS[:only_comments])
+ end
end
end
diff --git a/spec/helpers/one_trust_helper_spec.rb b/spec/helpers/one_trust_helper_spec.rb
index 85c38885304..20b731ac73d 100644
--- a/spec/helpers/one_trust_helper_spec.rb
+++ b/spec/helpers/one_trust_helper_spec.rb
@@ -4,11 +4,8 @@ require "spec_helper"
RSpec.describe OneTrustHelper do
describe '#one_trust_enabled?' do
- let(:user) { nil }
-
before do
stub_config(extra: { one_trust_id: SecureRandom.uuid })
- allow(helper).to receive(:current_user).and_return(user)
end
subject(:one_trust_enabled?) { helper.one_trust_enabled? }
@@ -18,20 +15,10 @@ RSpec.describe OneTrustHelper do
stub_feature_flags(ecomm_instrumentation: false)
end
- context 'when id is set and no user is set' do
- let(:user) { instance_double('User') }
-
- it { is_expected.to be_falsey }
- end
+ it { is_expected.to be_falsey }
end
context 'with ecomm_instrumentation feature flag enabled' do
- context 'when current user is set' do
- let(:user) { instance_double('User') }
-
- it { is_expected.to be_falsey }
- end
-
context 'when no id is set' do
before do
stub_config(extra: {})
@@ -39,10 +26,6 @@ RSpec.describe OneTrustHelper do
it { is_expected.to be_falsey }
end
-
- context 'when id is set and no user is set' do
- it { is_expected.to be_truthy }
- end
end
end
end
diff --git a/spec/helpers/projects/alert_management_helper_spec.rb b/spec/helpers/projects/alert_management_helper_spec.rb
index 2450f7838b3..0a5c4bedaa6 100644
--- a/spec/helpers/projects/alert_management_helper_spec.rb
+++ b/spec/helpers/projects/alert_management_helper_spec.rb
@@ -34,7 +34,6 @@ RSpec.describe Projects::AlertManagementHelper do
'empty-alert-svg-path' => match_asset_path('/assets/illustrations/alert-management-empty-state.svg'),
'user-can-enable-alert-management' => 'true',
'alert-management-enabled' => 'false',
- 'has-managed-prometheus' => 'false',
'text-query': nil,
'assignee-username-query': nil
)
@@ -45,52 +44,26 @@ RSpec.describe Projects::AlertManagementHelper do
let_it_be(:prometheus_integration) { create(:prometheus_integration, project: project) }
context 'when manual prometheus integration is active' do
- it "enables alert management and doesn't show managed prometheus" do
+ it "enables alert management" do
prometheus_integration.update!(manual_configuration: true)
expect(data).to include(
'alert-management-enabled' => 'true'
)
- expect(data).to include(
- 'has-managed-prometheus' => 'false'
- )
- end
- end
-
- context 'when a cluster prometheus is available' do
- let(:cluster) { create(:cluster, projects: [project]) }
-
- it 'has managed prometheus' do
- create(:clusters_integrations_prometheus, cluster: cluster)
-
- expect(data).to include(
- 'has-managed-prometheus' => 'true'
- )
end
end
- context 'when prometheus integration is inactive' do
- it 'disables alert management and hides managed prometheus' do
+ context 'when prometheus service is inactive' do
+ it 'disables alert management' do
prometheus_integration.update!(manual_configuration: false)
expect(data).to include(
'alert-management-enabled' => 'false'
)
- expect(data).to include(
- 'has-managed-prometheus' => 'false'
- )
end
end
end
- context 'without prometheus integration' do
- it "doesn't have managed prometheus" do
- expect(data).to include(
- 'has-managed-prometheus' => 'false'
- )
- end
- end
-
context 'with http integration' do
let_it_be(:integration) { create(:alert_management_http_integration, project: project) }
diff --git a/spec/helpers/projects/incidents_helper_spec.rb b/spec/helpers/projects/incidents_helper_spec.rb
index 7a8a6d5222f..d0dc18d56b0 100644
--- a/spec/helpers/projects/incidents_helper_spec.rb
+++ b/spec/helpers/projects/incidents_helper_spec.rb
@@ -5,7 +5,8 @@ require 'spec_helper'
RSpec.describe Projects::IncidentsHelper do
include Gitlab::Routing.url_helpers
- let(:project) { create(:project) }
+ let(:user) { build_stubbed(:user) }
+ let(:project) { build_stubbed(:project) }
let(:project_path) { project.full_path }
let(:new_issue_path) { new_project_issue_path(project) }
let(:issue_path) { project_issues_path(project) }
@@ -17,21 +18,43 @@ RSpec.describe Projects::IncidentsHelper do
}
end
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ allow(helper).to receive(:can?)
+ .with(user, :create_incident, project)
+ .and_return(can_create_incident)
+ end
+
describe '#incidents_data' do
subject(:data) { helper.incidents_data(project, params) }
- it 'returns frontend configuration' do
- expect(data).to include(
- 'project-path' => project_path,
- 'new-issue-path' => new_issue_path,
- 'incident-template-name' => 'incident',
- 'incident-type' => 'incident',
- 'issue-path' => issue_path,
- 'empty-list-svg-path' => match_asset_path('/assets/illustrations/incident-empty-state.svg'),
- 'text-query': 'search text',
- 'author-username-query': 'root',
- 'assignee-username-query': 'max.power'
- )
+ shared_examples 'frontend configuration' do
+ it 'returns frontend configuration' do
+ expect(data).to include(
+ 'project-path' => project_path,
+ 'new-issue-path' => new_issue_path,
+ 'incident-template-name' => 'incident',
+ 'incident-type' => 'incident',
+ 'issue-path' => issue_path,
+ 'empty-list-svg-path' => match_asset_path('/assets/illustrations/incident-empty-state.svg'),
+ 'text-query': 'search text',
+ 'author-username-query': 'root',
+ 'assignee-username-query': 'max.power',
+ 'can-create-incident': can_create_incident.to_s
+ )
+ end
+ end
+
+ context 'when user can create incidents' do
+ let(:can_create_incident) { true }
+
+ include_examples 'frontend configuration'
+ end
+
+ context 'when user cannot create incidents' do
+ let(:can_create_incident) { false }
+
+ include_examples 'frontend configuration'
end
end
end
diff --git a/spec/helpers/projects/security/configuration_helper_spec.rb b/spec/helpers/projects/security/configuration_helper_spec.rb
index c5049bd87f0..4c30ba87897 100644
--- a/spec/helpers/projects/security/configuration_helper_spec.rb
+++ b/spec/helpers/projects/security/configuration_helper_spec.rb
@@ -8,6 +8,6 @@ RSpec.describe Projects::Security::ConfigurationHelper do
describe 'security_upgrade_path' do
subject { security_upgrade_path }
- it { is_expected.to eq('https://about.gitlab.com/pricing/') }
+ it { is_expected.to eq("https://#{ApplicationHelper.promo_host}/pricing/") }
end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 5d52c9178cb..5d2af567549 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -268,7 +268,7 @@ RSpec.describe ProjectsHelper do
end
end
- describe '#link_to_set_password' do
+ describe '#no_password_message' do
let(:user) { create(:user, password_automatically_set: true) }
before do
@@ -276,18 +276,18 @@ RSpec.describe ProjectsHelper do
end
context 'password authentication is enabled for Git' do
- it 'returns link to set a password' do
+ it 'returns message prompting user to set password or set up a PAT' do
stub_application_setting(password_authentication_enabled_for_git?: true)
- expect(helper.link_to_set_password).to match %r{<a href="#{edit_profile_password_path}">set a password</a>}
+ expect(helper.no_password_message).to eq('Your account is authenticated with SSO or SAML. To <a href="/help/gitlab-basics/start-using-git#pull-and-push" target="_blank" rel="noopener noreferrer">push and pull</a> over HTTP with Git using this account, you must <a href="/-/profile/password/edit">set a password</a> or <a href="/-/profile/personal_access_tokens">set up a Personal Access Token</a> to use instead of a password. For more information, see <a href="/help/gitlab-basics/start-using-git#clone-with-https" target="_blank" rel="noopener noreferrer">Clone with HTTPS</a>.')
end
end
context 'password authentication is disabled for Git' do
- it 'returns link to create a personal access token' do
+ it 'returns message prompting user to set up a PAT' do
stub_application_setting(password_authentication_enabled_for_git?: false)
- expect(helper.link_to_set_password).to match %r{<a href="#{profile_personal_access_tokens_path}">create a personal access token</a>}
+ expect(helper.no_password_message).to eq('Your account is authenticated with SSO or SAML. To <a href="/help/gitlab-basics/start-using-git#pull-and-push" target="_blank" rel="noopener noreferrer">push and pull</a> over HTTP with Git using this account, you must <a href="/-/profile/personal_access_tokens">set up a Personal Access Token</a> to use instead of a password. For more information, see <a href="/help/gitlab-basics/start-using-git#clone-with-https" target="_blank" rel="noopener noreferrer">Clone with HTTPS</a>.')
end
end
end
@@ -983,4 +983,12 @@ RSpec.describe ProjectsHelper do
it { is_expected.not_to include('project-highlight-puc') }
end
end
+
+ describe "#delete_confirm_phrase" do
+ subject { helper.delete_confirm_phrase(project) }
+
+ it 'includes the project path with namespace' do
+ expect(subject).to eq(project.path_with_namespace)
+ end
+ end
end
diff --git a/spec/helpers/routing/pseudonymization_helper_spec.rb b/spec/helpers/routing/pseudonymization_helper_spec.rb
index a28a86d1f53..82ed893289d 100644
--- a/spec/helpers/routing/pseudonymization_helper_spec.rb
+++ b/spec/helpers/routing/pseudonymization_helper_spec.rb
@@ -25,95 +25,196 @@ RSpec.describe ::Routing::PseudonymizationHelper do
describe 'when url has params to mask' do
context 'with controller for MR' do
- let(:masked_url) { "http://test.host/namespace:#{group.id}/project:#{project.id}/-/merge_requests/#{merge_request.id}" }
+ let(:masked_url) { "http://localhost/namespace#{group.id}/project#{project.id}/-/merge_requests/#{merge_request.id}" }
+ let(:request) do
+ double(:Request,
+ path_parameters: {
+ controller: "projects/merge_requests",
+ action: "show",
+ namespace_id: group.name,
+ project_id: project.name,
+ id: merge_request.id.to_s
+ },
+ protocol: 'http',
+ host: 'localhost',
+ query_string: '')
+ end
before do
- allow(Rails.application.routes).to receive(:recognize_path).and_return({
- controller: "projects/merge_requests",
- action: "show",
- namespace_id: group.name,
- project_id: project.name,
- id: merge_request.id.to_s
- })
+ allow(helper).to receive(:request).and_return(request)
end
it_behaves_like 'masked url'
end
context 'with controller for issue' do
- let(:masked_url) { "http://test.host/namespace:#{group.id}/project:#{project.id}/-/issues/#{issue.id}" }
+ let(:masked_url) { "http://localhost/namespace#{group.id}/project#{project.id}/-/issues/#{issue.id}" }
+ let(:request) do
+ double(:Request,
+ path_parameters: {
+ controller: "projects/issues",
+ action: "show",
+ namespace_id: group.name,
+ project_id: project.name,
+ id: issue.id.to_s
+ },
+ protocol: 'http',
+ host: 'localhost',
+ query_string: '')
+ end
before do
- allow(Rails.application.routes).to receive(:recognize_path).and_return({
- controller: "projects/issues",
- action: "show",
- namespace_id: group.name,
- project_id: project.name,
- id: issue.id.to_s
- })
+ allow(helper).to receive(:request).and_return(request)
end
it_behaves_like 'masked url'
end
context 'with controller for groups with subgroups and project' do
- let(:masked_url) { "http://test.host/namespace:#{subgroup.id}/project:#{subproject.id}"}
+ let(:masked_url) { "http://localhost/namespace#{subgroup.id}/project#{subproject.id}"}
+ let(:request) do
+ double(:Request,
+ path_parameters: {
+ controller: 'projects',
+ action: 'show',
+ namespace_id: subgroup.name,
+ id: subproject.name
+ },
+ protocol: 'http',
+ host: 'localhost',
+ query_string: '')
+ end
before do
allow(helper).to receive(:group).and_return(subgroup)
allow(helper).to receive(:project).and_return(subproject)
- allow(Rails.application.routes).to receive(:recognize_path).and_return({
- controller: 'projects',
- action: 'show',
- namespace_id: subgroup.name,
- id: subproject.name
- })
+ allow(helper).to receive(:request).and_return(request)
end
it_behaves_like 'masked url'
end
context 'with controller for groups and subgroups' do
- let(:masked_url) { "http://test.host/namespace:#{subgroup.id}"}
+ let(:masked_url) { "http://localhost/groups/namespace#{subgroup.id}/-/shared"}
+ let(:request) do
+ double(:Request,
+ path_parameters: {
+ controller: 'groups',
+ action: 'show',
+ id: subgroup.name
+ },
+ protocol: 'http',
+ host: 'localhost',
+ query_string: '')
+ end
before do
allow(helper).to receive(:group).and_return(subgroup)
- allow(Rails.application.routes).to receive(:recognize_path).and_return({
- controller: 'groups',
- action: 'show',
- id: subgroup.name
- })
+ allow(helper).to receive(:request).and_return(request)
end
it_behaves_like 'masked url'
end
context 'with controller for blob with file path' do
- let(:masked_url) { "http://test.host/namespace:#{group.id}/project:#{project.id}/-/blob/:repository_path" }
+ let(:masked_url) { "http://localhost/namespace#{group.id}/project#{project.id}/-/blob/:repository_path" }
+ let(:request) do
+ double(:Request,
+ path_parameters: {
+ controller: 'projects/blob',
+ action: 'show',
+ namespace_id: group.name,
+ project_id: project.name,
+ id: 'master/README.md'
+ },
+ protocol: 'http',
+ host: 'localhost',
+ query_string: '')
+ end
+
+ before do
+ allow(helper).to receive(:request).and_return(request)
+ end
+
+ it_behaves_like 'masked url'
+ end
+
+ context 'when assignee_username is present' do
+ let(:masked_url) { "http://localhost/dashboard/issues?assignee_username=masked_assignee_username" }
+ let(:request) do
+ double(:Request,
+ path_parameters: {
+ controller: 'dashboard',
+ action: 'issues'
+ },
+ protocol: 'http',
+ host: 'localhost',
+ query_string: 'assignee_username=root')
+ end
before do
- allow(Rails.application.routes).to receive(:recognize_path).and_return({
- controller: 'projects/blob',
- action: 'show',
- namespace_id: group.name,
- project_id: project.name,
- id: 'master/README.md'
- })
+ allow(helper).to receive(:request).and_return(request)
end
it_behaves_like 'masked url'
end
- context 'with non identifiable controller' do
- let(:masked_url) { "http://test.host/dashboard/issues?assignee_username=root" }
+ context 'when author_username is present' do
+ let(:masked_url) { "http://localhost/dashboard/issues?author_username=masked_author_username&scope=masked_scope&state=masked_state" }
+ let(:request) do
+ double(:Request,
+ path_parameters: {
+ controller: 'dashboard',
+ action: 'issues'
+ },
+ protocol: 'http',
+ host: 'localhost',
+ query_string: 'author_username=root&scope=all&state=opened')
+ end
before do
- controller.request.path = '/dashboard/issues'
- controller.request.query_string = 'assignee_username=root'
- allow(Rails.application.routes).to receive(:recognize_path).and_return({
- controller: 'dashboard',
- action: 'issues'
- })
+ allow(helper).to receive(:request).and_return(request)
+ end
+
+ it_behaves_like 'masked url'
+ end
+
+ context 'when some query params are not required to be masked' do
+ let(:masked_url) { "http://localhost/dashboard/issues?author_username=masked_author_username&scope=all&state=masked_state" }
+ let(:request) do
+ double(:Request,
+ path_parameters: {
+ controller: 'dashboard',
+ action: 'issues'
+ },
+ protocol: 'http',
+ host: 'localhost',
+ query_string: 'author_username=root&scope=all&state=opened')
+ end
+
+ before do
+ stub_const('Routing::PseudonymizationHelper::MaskHelper::QUERY_PARAMS_TO_NOT_MASK', %w[scope].freeze)
+ allow(helper).to receive(:request).and_return(request)
+ end
+
+ it_behaves_like 'masked url'
+ end
+
+ context 'when query string has keys with the same names as path params' do
+ let(:masked_url) { "http://localhost/dashboard/issues?action=masked_action&scope=masked_scope&state=masked_state" }
+ let(:request) do
+ double(:Request,
+ path_parameters: {
+ controller: 'dashboard',
+ action: 'issues'
+ },
+ protocol: 'http',
+ host: 'localhost',
+ query_string: 'action=foobar&scope=all&state=opened')
+ end
+
+ before do
+ allow(helper).to receive(:request).and_return(request)
end
it_behaves_like 'masked url'
@@ -121,9 +222,13 @@ RSpec.describe ::Routing::PseudonymizationHelper do
end
describe 'when url has no params to mask' do
- let(:root_url) { 'http://test.host' }
+ let(:root_url) { 'http://localhost/some/path' }
context 'returns root url' do
+ before do
+ controller.request.path = 'some/path'
+ end
+
it 'masked_page_url' do
expect(helper.masked_page_url).to eq(root_url)
end
@@ -132,17 +237,26 @@ RSpec.describe ::Routing::PseudonymizationHelper do
describe 'when it raises exception' do
context 'calls error tracking' do
+ let(:request) do
+ double(:Request,
+ path_parameters: {
+ controller: 'dashboard',
+ action: 'issues'
+ },
+ protocol: 'http',
+ host: 'localhost',
+ query_string: 'assignee_username=root',
+ original_fullpath: '/dashboard/issues?assignee_username=root')
+ end
+
before do
- controller.request.path = '/dashboard/issues'
- controller.request.query_string = 'assignee_username=root'
- allow(Rails.application.routes).to receive(:recognize_path).and_return({
- controller: 'dashboard',
- action: 'issues'
- })
+ allow(helper).to receive(:request).and_return(request)
end
it 'sends error to sentry and returns nil' do
- allow(helper).to receive(:mask_params).with(anything).and_raise(ActionController::RoutingError, 'Some routing error')
+ allow_next_instance_of(Routing::PseudonymizationHelper::MaskHelper) do |mask_helper|
+ allow(mask_helper).to receive(:mask_params).and_raise(ActionController::RoutingError, 'Some routing error')
+ end
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
ActionController::RoutingError,
diff --git a/spec/helpers/storage_helper_spec.rb b/spec/helpers/storage_helper_spec.rb
index 2cec7203fe1..d0646b30161 100644
--- a/spec/helpers/storage_helper_spec.rb
+++ b/spec/helpers/storage_helper_spec.rb
@@ -27,17 +27,18 @@ RSpec.describe StorageHelper do
create(:project,
namespace: namespace,
statistics: build(:project_statistics,
- namespace: namespace,
- repository_size: 10.kilobytes,
- wiki_size: 10.bytes,
- lfs_objects_size: 20.gigabytes,
- build_artifacts_size: 30.megabytes,
- snippets_size: 40.megabytes,
- packages_size: 12.megabytes,
- uploads_size: 15.megabytes))
+ namespace: namespace,
+ repository_size: 10.kilobytes,
+ wiki_size: 10.bytes,
+ lfs_objects_size: 20.gigabytes,
+ build_artifacts_size: 30.megabytes,
+ pipeline_artifacts_size: 11.megabytes,
+ snippets_size: 40.megabytes,
+ packages_size: 12.megabytes,
+ uploads_size: 15.megabytes))
end
- let(:message) { 'Repository: 10 KB / Wikis: 10 Bytes / Build Artifacts: 30 MB / LFS: 20 GB / Snippets: 40 MB / Packages: 12 MB / Uploads: 15 MB' }
+ let(:message) { 'Repository: 10 KB / Wikis: 10 Bytes / Build Artifacts: 30 MB / Pipeline Artifacts: 11 MB / LFS: 20 GB / Snippets: 40 MB / Packages: 12 MB / Uploads: 15 MB' }
it 'works on ProjectStatistics' do
expect(helper.storage_counters_details(project.statistics)).to eq(message)
diff --git a/spec/helpers/tab_helper_spec.rb b/spec/helpers/tab_helper_spec.rb
index 346bfc7850c..e5e88466946 100644
--- a/spec/helpers/tab_helper_spec.rb
+++ b/spec/helpers/tab_helper_spec.rb
@@ -36,7 +36,15 @@ RSpec.describe TabHelper do
expect(gl_tab_link_to('/url') { 'block content' }).to match(/block content/)
end
- it 'creates a tab with custom classes' do
+ it 'creates a tab with custom classes for enclosing list item without content block provided' do
+ expect(gl_tab_link_to('Link', '/url', { tab_class: 'my-class' })).to match(/<li class=".*my-class.*"/)
+ end
+
+ it 'creates a tab with custom classes for enclosing list item with content block provided' do
+ expect(gl_tab_link_to('/url', { tab_class: 'my-class' }) { 'Link' }).to match(/<li class=".*my-class.*"/)
+ end
+
+ it 'creates a tab with custom classes for anchor element' do
expect(gl_tab_link_to('Link', '/url', { class: 'my-class' })).to match(/<a class=".*my-class.*"/)
end
@@ -150,4 +158,22 @@ RSpec.describe TabHelper do
end
end
end
+
+ describe 'gl_tab_counter_badge' do
+ it 'creates a tab counter badge' do
+ expect(gl_tab_counter_badge(1)).to eq('<span class="badge badge-muted badge-pill gl-badge sm gl-tab-counter-badge">1</span>')
+ end
+
+ context 'with extra classes' do
+ it 'creates a tab counter badge with the correct class attribute' do
+ expect(gl_tab_counter_badge(1, { class: 'js-test' })).to eq('<span class="js-test badge badge-muted badge-pill gl-badge sm gl-tab-counter-badge">1</span>')
+ end
+ end
+
+ context 'with data attributes' do
+ it 'creates a tab counter badge with the data attributes' do
+ expect(gl_tab_counter_badge(1, { data: { some_attribute: 'foo' } })).to eq('<span class="badge badge-muted badge-pill gl-badge sm gl-tab-counter-badge" data-some-attribute="foo">1</span>')
+ end
+ end
+ end
end
diff --git a/spec/helpers/terms_helper_spec.rb b/spec/helpers/terms_helper_spec.rb
new file mode 100644
index 00000000000..9120aad4627
--- /dev/null
+++ b/spec/helpers/terms_helper_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe TermsHelper do
+ let_it_be(:current_user) { build(:user) }
+ let_it_be(:terms) { build(:term) }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(current_user)
+ end
+
+ describe '#terms_data' do
+ let_it_be(:redirect) { '%2F' }
+ let_it_be(:terms_markdown) { 'Lorem ipsum dolor sit amet' }
+ let_it_be(:accept_path) { '/-/users/terms/14/accept?redirect=%2F' }
+ let_it_be(:decline_path) { '/-/users/terms/14/decline?redirect=%2F' }
+
+ subject(:result) { Gitlab::Json.parse(helper.terms_data(terms, redirect)) }
+
+ it 'returns correct json' do
+ expect(helper).to receive(:markdown_field).with(terms, :terms).and_return(terms_markdown)
+ expect(helper).to receive(:can?).with(current_user, :accept_terms, terms).and_return(true)
+ expect(helper).to receive(:can?).with(current_user, :decline_terms, terms).and_return(true)
+ expect(helper).to receive(:accept_term_path).with(terms, { redirect: redirect }).and_return(accept_path)
+ expect(helper).to receive(:decline_term_path).with(terms, { redirect: redirect }).and_return(decline_path)
+
+ expected = {
+ terms: terms_markdown,
+ permissions: {
+ can_accept: true,
+ can_decline: true
+ },
+ paths: {
+ accept: accept_path,
+ decline: decline_path,
+ root: root_path
+ }
+ }.as_json
+
+ expect(result).to eq(expected)
+ end
+ end
+end
diff --git a/spec/helpers/time_zone_helper_spec.rb b/spec/helpers/time_zone_helper_spec.rb
index 43ad130c4b5..006fae5b814 100644
--- a/spec/helpers/time_zone_helper_spec.rb
+++ b/spec/helpers/time_zone_helper_spec.rb
@@ -100,4 +100,36 @@ RSpec.describe TimeZoneHelper, :aggregate_failures do
end
end
end
+
+ describe '#local_time_instance' do
+ let_it_be(:timezone) { 'UTC' }
+
+ before do
+ travel_to Time.find_zone(timezone).local(2021, 7, 20, 15, 30, 45)
+ end
+
+ context 'when timezone is `nil`' do
+ it 'returns the system timezone instance' do
+ expect(helper.local_time_instance(nil).name).to eq(timezone)
+ end
+ end
+
+ context 'when timezone is blank' do
+ it 'returns the system timezone instance' do
+ expect(helper.local_time_instance('').name).to eq(timezone)
+ end
+ end
+
+ context 'when a valid timezone is passed' do
+ it 'returns the local time instance' do
+ expect(helper.local_time_instance('America/Los_Angeles').name).to eq('America/Los_Angeles')
+ end
+ end
+
+ context 'when an invalid timezone is passed' do
+ it 'returns the system timezone instance' do
+ expect(helper.local_time_instance('Foo/Bar').name).to eq(timezone)
+ end
+ end
+ end
end
diff --git a/spec/helpers/user_callouts_helper_spec.rb b/spec/helpers/user_callouts_helper_spec.rb
index f738ba855b8..7abc67e29a4 100644
--- a/spec/helpers/user_callouts_helper_spec.rb
+++ b/spec/helpers/user_callouts_helper_spec.rb
@@ -216,20 +216,6 @@ RSpec.describe UserCalloutsHelper do
context 'when the invite_members_banner has not been dismissed' do
it { is_expected.to eq(true) }
- context 'when a user has dismissed this banner via cookies already' do
- before do
- helper.request.cookies["invite_#{group.id}_#{user.id}"] = 'true'
- end
-
- it { is_expected.to eq(false) }
-
- it 'creates the callout from cookie', :aggregate_failures do
- expect { subject }.to change { Users::GroupCallout.count }.by(1)
- expect(Users::GroupCallout.last).to have_attributes(group_id: group.id,
- feature_name: described_class::INVITE_MEMBERS_BANNER)
- end
- end
-
context 'when the group was just created' do
before do
flash[:notice] = "Group #{group.name} was successfully created"
diff --git a/spec/helpers/users_helper_spec.rb b/spec/helpers/users_helper_spec.rb
index 480b1e2a0de..2b55319c70c 100644
--- a/spec/helpers/users_helper_spec.rb
+++ b/spec/helpers/users_helper_spec.rb
@@ -383,7 +383,7 @@ RSpec.describe UsersHelper do
end
context 'when `user.unconfirmed_email` is set' do
- let(:user) { create(:user, unconfirmed_email: 'foo@bar.com') }
+ let(:user) { create(:user, :unconfirmed, unconfirmed_email: 'foo@bar.com') }
it 'sets `modal_attributes.messageHtml` correctly' do
expect(Gitlab::Json.parse(confirm_user_data[:modal_attributes])['messageHtml']).to eq('This user has an unconfirmed email address (foo@bar.com). You may force a confirmation.')
diff --git a/spec/helpers/wiki_helper_spec.rb b/spec/helpers/wiki_helper_spec.rb
index dc76f92db1b..0d04ca2b876 100644
--- a/spec/helpers/wiki_helper_spec.rb
+++ b/spec/helpers/wiki_helper_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe WikiHelper do
it 'sets the title for the show action' do
expect(helper).to receive(:breadcrumb_title).with(page.human_title)
- expect(helper).to receive(:wiki_breadcrumb_dropdown_links).with(page.slug)
+ expect(helper).to receive(:wiki_breadcrumb_collapsed_links).with(page.slug)
expect(helper).to receive(:page_title).with(page.human_title, 'Wiki')
expect(helper).to receive(:add_to_breadcrumbs).with('Wiki', helper.wiki_path(page.wiki))
@@ -17,7 +17,7 @@ RSpec.describe WikiHelper do
it 'sets the title for a custom action' do
expect(helper).to receive(:breadcrumb_title).with(page.human_title)
- expect(helper).to receive(:wiki_breadcrumb_dropdown_links).with(page.slug)
+ expect(helper).to receive(:wiki_breadcrumb_collapsed_links).with(page.slug)
expect(helper).to receive(:page_title).with('Edit', page.human_title, 'Wiki')
expect(helper).to receive(:add_to_breadcrumbs).with('Wiki', helper.wiki_path(page.wiki))
@@ -27,7 +27,7 @@ RSpec.describe WikiHelper do
it 'sets the title for an unsaved page' do
expect(page).to receive(:persisted?).and_return(false)
expect(helper).not_to receive(:breadcrumb_title)
- expect(helper).not_to receive(:wiki_breadcrumb_dropdown_links)
+ expect(helper).not_to receive(:wiki_breadcrumb_collapsed_links)
expect(helper).to receive(:page_title).with('Wiki')
expect(helper).to receive(:add_to_breadcrumbs).with('Wiki', helper.wiki_path(page.wiki))
diff --git a/spec/initializers/0_postgresql_types_spec.rb b/spec/initializers/0_postgresql_types_spec.rb
new file mode 100644
index 00000000000..76b243033d0
--- /dev/null
+++ b/spec/initializers/0_postgresql_types_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'PostgreSQL registered types' do
+ subject(:types) { ApplicationRecord.connection.send(:type_map).keys }
+
+ # These can be obtained via SELECT oid, typname from pg_type
+ it 'includes custom and standard OIDs' do
+ expect(types).to include(28, 194, 1034, 3220, 23, 20)
+ end
+
+ it 'includes custom and standard types' do
+ expect(types).to include('xid', 'pg_node_tree', '_aclitem', 'pg_lsn', 'int4', 'int8')
+ end
+end
diff --git a/spec/initializers/100_patch_omniauth_oauth2_spec.rb b/spec/initializers/100_patch_omniauth_oauth2_spec.rb
index 0c436e4ef45..c30a1cdeafa 100644
--- a/spec/initializers/100_patch_omniauth_oauth2_spec.rb
+++ b/spec/initializers/100_patch_omniauth_oauth2_spec.rb
@@ -2,12 +2,10 @@
require 'spec_helper'
-RSpec.describe 'OmniAuth::Strategies::OAuth2', type: :strategy do
- let(:strategy) { [OmniAuth::Strategies::OAuth2] }
-
+RSpec.describe 'OmniAuth::Strategies::OAuth2' do
it 'verifies the gem version' do
current_version = OmniAuth::OAuth2::VERSION
- expected_version = '1.7.1'
+ expected_version = '1.7.2'
expect(current_version).to eq(expected_version), <<~EOF
New version #{current_version} of the `omniauth-oauth2` gem detected!
@@ -18,39 +16,18 @@ RSpec.describe 'OmniAuth::Strategies::OAuth2', type: :strategy do
EOF
end
- context 'when a custom error message is passed from an OAuth2 provider' do
- let(:message) { 'Please go to https://evil.com' }
- let(:state) { 'secret' }
- let(:callback_path) { '/users/auth/oauth2/callback' }
- let(:params) { { state: state, error: 'evil_key', error_description: message } }
- let(:error) { last_request.env['omniauth.error'] }
-
- before do
- env('rack.session', { 'omniauth.state' => state })
- end
-
- it 'returns the custom error message if the state is valid' do
- get callback_path, **params
-
- expect(error.message).to eq("evil_key | #{message}")
- end
+ context 'when a Faraday exception is raised' do
+ where(exception: [Faraday::TimeoutError, Faraday::ConnectionFailed])
- it 'returns the custom `error_reason` message if the `error_description` is blank' do
- get callback_path, **params.merge(error_description: ' ', error_reason: 'custom reason')
-
- expect(error.message).to eq('evil_key | custom reason')
- end
-
- it 'returns a CSRF error if the state is invalid' do
- get callback_path, **params.merge(state: 'invalid')
-
- expect(error.message).to eq('csrf_detected | CSRF detected')
- end
+ with_them do
+ it 'passes the exception to OmniAuth' do
+ instance = OmniAuth::Strategies::OAuth2.new(double)
- it 'returns a CSRF error if the state is missing' do
- get callback_path, **params.without(:state)
+ expect(instance).to receive(:original_callback_phase) { raise exception, 'message' }
+ expect(instance).to receive(:fail!).with(:timeout, kind_of(exception))
- expect(error.message).to eq('csrf_detected | CSRF detected')
+ instance.callback_phase
+ end
end
end
end
diff --git a/spec/initializers/carrierwave_patch_spec.rb b/spec/initializers/carrierwave_patch_spec.rb
index e219db2299d..b0f337935ef 100644
--- a/spec/initializers/carrierwave_patch_spec.rb
+++ b/spec/initializers/carrierwave_patch_spec.rb
@@ -15,9 +15,6 @@ RSpec.describe 'CarrierWave::Storage::Fog::File' do
subject { CarrierWave::Storage::Fog::File.new(uploader, storage, test_filename) }
before do
- require 'fog/azurerm'
- require 'fog/aws'
-
stub_object_storage(connection_params: connection_options, remote_directory: bucket_name)
allow(uploader).to receive(:fog_directory).and_return(bucket_name)
diff --git a/spec/initializers/database_config_spec.rb b/spec/initializers/database_config_spec.rb
index 23f7fd06254..230f1296760 100644
--- a/spec/initializers/database_config_spec.rb
+++ b/spec/initializers/database_config_spec.rb
@@ -7,56 +7,15 @@ RSpec.describe 'Database config initializer', :reestablished_active_record_base
load Rails.root.join('config/initializers/database_config.rb')
end
- before do
- allow(Gitlab::Runtime).to receive(:max_threads).and_return(max_threads)
- end
-
- let(:max_threads) { 8 }
-
it 'retains the correct database name for the connection' do
- previous_db_name = Gitlab::Database.main.scope.connection.pool.db_config.name
+ previous_db_name = ApplicationRecord.connection.pool.db_config.name
subject
- expect(Gitlab::Database.main.scope.connection.pool.db_config.name).to eq(previous_db_name)
+ expect(ApplicationRecord.connection.pool.db_config.name).to eq(previous_db_name)
end
- context 'when no custom headroom is specified' do
- it 'sets the pool size based on the number of worker threads' do
- old = ActiveRecord::Base.connection_db_config.pool
-
- expect(old).not_to eq(18)
-
- expect { subject }
- .to change { ActiveRecord::Base.connection_db_config.pool }
- .from(old)
- .to(18)
- end
-
- it 'overwrites custom pool settings' do
- config = Gitlab::Database.main.config.merge(pool: 42)
-
- allow(Gitlab::Database.main).to receive(:config).and_return(config)
- subject
-
- expect(ActiveRecord::Base.connection_db_config.pool).to eq(18)
- end
- end
-
- context "when specifying headroom through an ENV variable" do
- let(:headroom) { 15 }
-
- before do
- stub_env("DB_POOL_HEADROOM", headroom)
- end
-
- it "adds headroom on top of the calculated size" do
- old = ActiveRecord::Base.connection_db_config.pool
-
- expect { subject }
- .to change { ActiveRecord::Base.connection_db_config.pool }
- .from(old)
- .to(23)
- end
+ it 'does not overwrite custom pool settings' do
+ expect { subject }.not_to change { ActiveRecord::Base.connection_db_config.pool }
end
end
diff --git a/spec/initializers/session_store_spec.rb b/spec/initializers/session_store_spec.rb
new file mode 100644
index 00000000000..3da52ccc981
--- /dev/null
+++ b/spec/initializers/session_store_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Session initializer for GitLab' do
+ subject { Gitlab::Application.config }
+
+ let(:load_session_store) do
+ load Rails.root.join('config/initializers/session_store.rb')
+ end
+
+ describe 'config#session_store' do
+ context 'when the GITLAB_REDIS_STORE_WITH_SESSION_STORE env is not set' do
+ before do
+ stub_env('GITLAB_REDIS_STORE_WITH_SESSION_STORE', nil)
+ end
+
+ it 'initialized as a redis_store with a proper Redis::Store instance' do
+ expect(subject).to receive(:session_store).with(:redis_store, a_hash_including(redis_store: kind_of(::Redis::Store)))
+
+ load_session_store
+ end
+ end
+
+ context 'when the GITLAB_REDIS_STORE_WITH_SESSION_STORE env is disabled' do
+ before do
+ stub_env('GITLAB_REDIS_STORE_WITH_SESSION_STORE', false)
+ end
+
+ it 'initialized as a redis_store with a proper servers configuration' do
+ expect(subject).to receive(:session_store).with(:redis_store, a_hash_including(servers: kind_of(Hash)))
+
+ load_session_store
+ end
+ end
+ end
+end
diff --git a/spec/lib/api/ci/helpers/runner_spec.rb b/spec/lib/api/ci/helpers/runner_spec.rb
index cc871d66d40..37277e7dcbd 100644
--- a/spec/lib/api/ci/helpers/runner_spec.rb
+++ b/spec/lib/api/ci/helpers/runner_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe API::Ci::Helpers::Runner do
it 'handles sticking of a build when a build ID is specified' do
allow(helper).to receive(:params).and_return(id: build.id)
- expect(ApplicationRecord.sticking)
+ expect(Ci::Build.sticking)
.to receive(:stick_or_unstick_request)
.with({}, :build, build.id)
@@ -25,7 +25,7 @@ RSpec.describe API::Ci::Helpers::Runner do
it 'does not handle sticking if no build ID was specified' do
allow(helper).to receive(:params).and_return({})
- expect(ApplicationRecord.sticking)
+ expect(Ci::Build.sticking)
.not_to receive(:stick_or_unstick_request)
helper.current_job
@@ -44,7 +44,7 @@ RSpec.describe API::Ci::Helpers::Runner do
it 'handles sticking of a runner if a token is specified' do
allow(helper).to receive(:params).and_return(token: runner.token)
- expect(ApplicationRecord.sticking)
+ expect(Ci::Runner.sticking)
.to receive(:stick_or_unstick_request)
.with({}, :runner, runner.token)
@@ -54,7 +54,7 @@ RSpec.describe API::Ci::Helpers::Runner do
it 'does not handle sticking if no token was specified' do
allow(helper).to receive(:params).and_return({})
- expect(ApplicationRecord.sticking)
+ expect(Ci::Runner.sticking)
.not_to receive(:stick_or_unstick_request)
helper.current_runner
diff --git a/spec/lib/api/entities/projects/topic_spec.rb b/spec/lib/api/entities/projects/topic_spec.rb
new file mode 100644
index 00000000000..cdf142dbb7d
--- /dev/null
+++ b/spec/lib/api/entities/projects/topic_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Entities::Projects::Topic do
+ let(:topic) { create(:topic) }
+
+ subject { described_class.new(topic).as_json }
+
+ it 'exposes correct attributes' do
+ expect(subject).to include(
+ :id,
+ :name,
+ :description,
+ :total_projects_count,
+ :avatar_url
+ )
+ end
+end
diff --git a/spec/lib/api/helpers_spec.rb b/spec/lib/api/helpers_spec.rb
index 37e040a422b..2277bd78e86 100644
--- a/spec/lib/api/helpers_spec.rb
+++ b/spec/lib/api/helpers_spec.rb
@@ -351,12 +351,14 @@ RSpec.describe API::Helpers do
let(:send_git_blob) do
subject.send(:send_git_blob, repository, blob)
+ subject.header
end
before do
allow(subject).to receive(:env).and_return({})
allow(subject).to receive(:content_type)
allow(subject).to receive(:header).and_return({})
+ allow(subject).to receive(:body).and_return('')
allow(Gitlab::Workhorse).to receive(:send_git_blob)
end
diff --git a/spec/lib/atlassian/jira_connect/client_spec.rb b/spec/lib/atlassian/jira_connect/client_spec.rb
index 5c8d4282118..9201d1c5dcb 100644
--- a/spec/lib/atlassian/jira_connect/client_spec.rb
+++ b/spec/lib/atlassian/jira_connect/client_spec.rb
@@ -18,7 +18,15 @@ RSpec.describe Atlassian::JiraConnect::Client do
end
end
- describe '.generate_update_sequence_id' do
+ around do |example|
+ if example.metadata[:skip_freeze_time]
+ example.run
+ else
+ freeze_time { example.run }
+ end
+ end
+
+ describe '.generate_update_sequence_id', :skip_freeze_time do
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)
diff --git a/spec/lib/banzai/filter/emoji_filter_spec.rb b/spec/lib/banzai/filter/emoji_filter_spec.rb
index cb0b470eaa1..d621f63211b 100644
--- a/spec/lib/banzai/filter/emoji_filter_spec.rb
+++ b/spec/lib/banzai/filter/emoji_filter_spec.rb
@@ -28,9 +28,9 @@ RSpec.describe Banzai::Filter::EmojiFilter do
it 'replaces name versions of trademark, copyright, and registered trademark' do
doc = filter('<p>:tm: :copyright: :registered:</p>')
- expect(doc.css('gl-emoji')[0].text).to eq '™'
- expect(doc.css('gl-emoji')[1].text).to eq '©'
- expect(doc.css('gl-emoji')[2].text).to eq '®'
+ expect(doc.css('gl-emoji')[0].text).to eq '™️'
+ expect(doc.css('gl-emoji')[1].text).to eq '©️'
+ expect(doc.css('gl-emoji')[2].text).to eq '®️'
end
it 'correctly encodes the URL' do
diff --git a/spec/lib/banzai/filter/footnote_filter_spec.rb b/spec/lib/banzai/filter/footnote_filter_spec.rb
index 01b7319fab1..54faa748d53 100644
--- a/spec/lib/banzai/filter/footnote_filter_spec.rb
+++ b/spec/lib/banzai/filter/footnote_filter_spec.rb
@@ -5,34 +5,42 @@ require 'spec_helper'
RSpec.describe Banzai::Filter::FootnoteFilter do
include FilterSpecHelper
- # first[^1] and second[^second]
+ # rubocop:disable Style/AsciiComments
+ # first[^1] and second[^second] and third[^_😄_]
# [^1]: one
# [^second]: two
+ # [^_😄_]: three
+ # rubocop:enable Style/AsciiComments
let(:footnote) do
- <<~EOF
- <p>first<sup><a href="#fn1" id="fnref1">1</a></sup> and second<sup><a href="#fn2" id="fnref2">2</a></sup></p>
- <p>same reference<sup><a href="#fn1" id="fnref1">1</a></sup></p>
+ <<~EOF.strip_heredoc
+ <p>first<sup><a href="#fn-1" id="fnref-1">1</a></sup> and second<sup><a href="#fn-second" id="fnref-second">2</a></sup> and third<sup><a href="#fn-_%F0%9F%98%84_" id="fnref-_%F0%9F%98%84_">3</a></sup></p>
+
<ol>
- <li id="fn1">
- <p>one <a href="#fnref1">↩</a></p>
+ <li id="fn-1">
+ <p>one <a href="#fnref-1" aria-label="Back to content">↩</a></p>
</li>
- <li id="fn2">
- <p>two <a href="#fnref2">↩</a></p>
+ <li id="fn-second">
+ <p>two <a href="#fnref-second" aria-label="Back to content">↩</a></p>
+ </li>\n<li id="fn-_%F0%9F%98%84_">
+ <p>three <a href="#fnref-_%F0%9F%98%84_" aria-label="Back to content">↩</a></p>
</li>
</ol>
EOF
end
let(:filtered_footnote) do
- <<~EOF
- <p>first<sup class="footnote-ref"><a href="#fn1-#{identifier}" id="fnref1-#{identifier}">1</a></sup> and second<sup class="footnote-ref"><a href="#fn2-#{identifier}" id="fnref2-#{identifier}">2</a></sup></p>
- <p>same reference<sup class="footnote-ref"><a href="#fn1-#{identifier}" id="fnref1-#{identifier}">1</a></sup></p>
- <section class="footnotes"><ol>
- <li id="fn1-#{identifier}">
- <p>one <a href="#fnref1-#{identifier}" class="footnote-backref">↩</a></p>
+ <<~EOF.strip_heredoc
+ <p>first<sup class="footnote-ref"><a href="#fn-1-#{identifier}" id="fnref-1-#{identifier}" data-footnote-ref="">1</a></sup> and second<sup class="footnote-ref"><a href="#fn-second-#{identifier}" id="fnref-second-#{identifier}" data-footnote-ref="">2</a></sup> and third<sup class="footnote-ref"><a href="#fn-_%F0%9F%98%84_-#{identifier}" id="fnref-_%F0%9F%98%84_-#{identifier}" data-footnote-ref="">3</a></sup></p>
+
+ <section class=\"footnotes\" data-footnotes><ol>
+ <li id="fn-1-#{identifier}">
+ <p>one <a href="#fnref-1-#{identifier}" aria-label="Back to content" class="footnote-backref" data-footnote-backref="">↩</a></p>
+ </li>
+ <li id="fn-second-#{identifier}">
+ <p>two <a href="#fnref-second-#{identifier}" aria-label="Back to content" class="footnote-backref" data-footnote-backref="">↩</a></p>
</li>
- <li id="fn2-#{identifier}">
- <p>two <a href="#fnref2-#{identifier}" class="footnote-backref">↩</a></p>
+ <li id="fn-_%F0%9F%98%84_-#{identifier}">
+ <p>three <a href="#fnref-_%F0%9F%98%84_-#{identifier}" aria-label="Back to content" class="footnote-backref" data-footnote-backref="">↩</a></p>
</li>
</ol></section>
EOF
@@ -41,10 +49,56 @@ RSpec.describe Banzai::Filter::FootnoteFilter do
context 'when footnotes exist' do
let(:doc) { filter(footnote) }
let(:link_node) { doc.css('sup > a').first }
- let(:identifier) { link_node[:id].delete_prefix('fnref1-') }
+ let(:identifier) { link_node[:id].delete_prefix('fnref-1-') }
it 'properly adds the necessary ids and classes' do
expect(doc.to_html).to eq filtered_footnote
end
+
+ context 'using ruby-based HTML renderer' do
+ # first[^1] and second[^second]
+ # [^1]: one
+ # [^second]: two
+ let(:footnote) do
+ <<~EOF
+ <p>first<sup><a href="#fn1" id="fnref1">1</a></sup> and second<sup><a href="#fn2" id="fnref2">2</a></sup></p>
+ <p>same reference<sup><a href="#fn1" id="fnref1">1</a></sup></p>
+ <ol>
+ <li id="fn1">
+ <p>one <a href="#fnref1">↩</a></p>
+ </li>
+ <li id="fn2">
+ <p>two <a href="#fnref2">↩</a></p>
+ </li>
+ </ol>
+ EOF
+ end
+
+ let(:filtered_footnote) do
+ <<~EOF
+ <p>first<sup class="footnote-ref"><a href="#fn1-#{identifier}" id="fnref1-#{identifier}">1</a></sup> and second<sup class="footnote-ref"><a href="#fn2-#{identifier}" id="fnref2-#{identifier}">2</a></sup></p>
+ <p>same reference<sup class="footnote-ref"><a href="#fn1-#{identifier}" id="fnref1-#{identifier}">1</a></sup></p>
+ <section class="footnotes"><ol>
+ <li id="fn1-#{identifier}">
+ <p>one <a href="#fnref1-#{identifier}" class="footnote-backref">↩</a></p>
+ </li>
+ <li id="fn2-#{identifier}">
+ <p>two <a href="#fnref2-#{identifier}" class="footnote-backref">↩</a></p>
+ </li>
+ </ol></section>
+ EOF
+ end
+
+ let(:doc) { filter(footnote) }
+ let(:identifier) { link_node[:id].delete_prefix('fnref1-') }
+
+ before do
+ stub_feature_flags(use_cmark_renderer: false)
+ end
+
+ it 'properly adds the necessary ids and classes' do
+ expect(doc.to_html).to eq filtered_footnote
+ end
+ end
end
end
diff --git a/spec/lib/banzai/filter/markdown_filter_spec.rb b/spec/lib/banzai/filter/markdown_filter_spec.rb
index c5e84a0c1e7..a310de5c015 100644
--- a/spec/lib/banzai/filter/markdown_filter_spec.rb
+++ b/spec/lib/banzai/filter/markdown_filter_spec.rb
@@ -5,90 +5,125 @@ require 'spec_helper'
RSpec.describe Banzai::Filter::MarkdownFilter do
include FilterSpecHelper
- describe 'markdown engine from context' do
- it 'defaults to CommonMark' do
- expect_next_instance_of(Banzai::Filter::MarkdownEngines::CommonMark) do |instance|
- expect(instance).to receive(:render).and_return('test')
+ shared_examples_for 'renders correct markdown' do
+ describe 'markdown engine from context' do
+ it 'defaults to CommonMark' do
+ expect_next_instance_of(Banzai::Filter::MarkdownEngines::CommonMark) do |instance|
+ expect(instance).to receive(:render).and_return('test')
+ end
+
+ filter('test')
end
- filter('test')
- end
+ it 'uses CommonMark' do
+ expect_next_instance_of(Banzai::Filter::MarkdownEngines::CommonMark) do |instance|
+ expect(instance).to receive(:render).and_return('test')
+ end
- it 'uses CommonMark' do
- expect_next_instance_of(Banzai::Filter::MarkdownEngines::CommonMark) do |instance|
- expect(instance).to receive(:render).and_return('test')
+ filter('test', { markdown_engine: :common_mark })
end
-
- filter('test', { markdown_engine: :common_mark })
end
- end
- describe 'code block' do
- context 'using CommonMark' do
- before do
- stub_const('Banzai::Filter::MarkdownFilter::DEFAULT_ENGINE', :common_mark)
+ describe 'code block' do
+ context 'using CommonMark' do
+ before do
+ stub_const('Banzai::Filter::MarkdownFilter::DEFAULT_ENGINE', :common_mark)
+ end
+
+ it 'adds language to lang attribute when specified' do
+ result = filter("```html\nsome code\n```", no_sourcepos: true)
+
+ if Feature.enabled?(:use_cmark_renderer)
+ expect(result).to start_with('<pre lang="html"><code>')
+ else
+ expect(result).to start_with('<pre><code lang="html">')
+ end
+ end
+
+ it 'does not add language to lang attribute when not specified' do
+ result = filter("```\nsome code\n```", no_sourcepos: true)
+
+ expect(result).to start_with('<pre><code>')
+ end
+
+ it 'works with utf8 chars in language' do
+ result = filter("```日\nsome code\n```", no_sourcepos: true)
+
+ if Feature.enabled?(:use_cmark_renderer)
+ expect(result).to start_with('<pre lang="日"><code>')
+ else
+ expect(result).to start_with('<pre><code lang="日">')
+ end
+ end
+
+ it 'works with additional language parameters' do
+ result = filter("```ruby:red gem foo\nsome code\n```", no_sourcepos: true)
+
+ if Feature.enabled?(:use_cmark_renderer)
+ expect(result).to start_with('<pre lang="ruby:red" data-meta="gem foo"><code>')
+ else
+ expect(result).to start_with('<pre><code lang="ruby:red gem foo">')
+ end
+ end
end
+ end
- it 'adds language to lang attribute when specified' do
- result = filter("```html\nsome code\n```", no_sourcepos: true)
-
- expect(result).to start_with('<pre><code lang="html">')
- end
-
- it 'does not add language to lang attribute when not specified' do
- result = filter("```\nsome code\n```", no_sourcepos: true)
-
- expect(result).to start_with('<pre><code>')
- end
+ describe 'source line position' do
+ context 'using CommonMark' do
+ before do
+ stub_const('Banzai::Filter::MarkdownFilter::DEFAULT_ENGINE', :common_mark)
+ end
- it 'works with utf8 chars in language' do
- result = filter("```日\nsome code\n```", no_sourcepos: true)
+ it 'defaults to add data-sourcepos' do
+ result = filter('test')
- expect(result).to start_with('<pre><code lang="日">')
- end
+ expect(result).to eq '<p data-sourcepos="1:1-1:4">test</p>'
+ end
- it 'works with additional language parameters' do
- result = filter("```ruby:red gem\nsome code\n```", no_sourcepos: true)
+ it 'disables data-sourcepos' do
+ result = filter('test', no_sourcepos: true)
- expect(result).to start_with('<pre><code lang="ruby:red gem">')
+ expect(result).to eq '<p>test</p>'
+ end
end
end
- end
- describe 'source line position' do
- context 'using CommonMark' do
- before do
- stub_const('Banzai::Filter::MarkdownFilter::DEFAULT_ENGINE', :common_mark)
- end
+ describe 'footnotes in tables' do
+ it 'processes footnotes in table cells' do
+ text = <<-MD.strip_heredoc
+ | Column1 |
+ | --------- |
+ | foot [^1] |
- it 'defaults to add data-sourcepos' do
- result = filter('test')
+ [^1]: a footnote
+ MD
- expect(result).to eq '<p data-sourcepos="1:1-1:4">test</p>'
- end
+ result = filter(text, no_sourcepos: true)
- it 'disables data-sourcepos' do
- result = filter('test', no_sourcepos: true)
+ expect(result).to include('<td>foot <sup')
- expect(result).to eq '<p>test</p>'
+ if Feature.enabled?(:use_cmark_renderer)
+ expect(result).to include('<section class="footnotes" data-footnotes>')
+ else
+ expect(result).to include('<section class="footnotes">')
+ end
end
end
end
- describe 'footnotes in tables' do
- it 'processes footnotes in table cells' do
- text = <<-MD.strip_heredoc
- | Column1 |
- | --------- |
- | foot [^1] |
-
- [^1]: a footnote
- MD
+ context 'using ruby-based HTML renderer' do
+ before do
+ stub_feature_flags(use_cmark_renderer: false)
+ end
- result = filter(text, no_sourcepos: true)
+ it_behaves_like 'renders correct markdown'
+ end
- expect(result).to include('<td>foot <sup')
- expect(result).to include('<section class="footnotes">')
+ context 'using c-based HTML renderer' do
+ before do
+ stub_feature_flags(use_cmark_renderer: true)
end
+
+ it_behaves_like 'renders correct markdown'
end
end
diff --git a/spec/lib/banzai/filter/plantuml_filter_spec.rb b/spec/lib/banzai/filter/plantuml_filter_spec.rb
index 5ad94c74514..d1a3b5689a8 100644
--- a/spec/lib/banzai/filter/plantuml_filter_spec.rb
+++ b/spec/lib/banzai/filter/plantuml_filter_spec.rb
@@ -5,30 +5,67 @@ require 'spec_helper'
RSpec.describe Banzai::Filter::PlantumlFilter do
include FilterSpecHelper
- it 'replaces plantuml pre tag with img tag' do
- stub_application_setting(plantuml_enabled: true, plantuml_url: "http://localhost:8080")
- input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>'
- output = '<div class="imageblock"><div class="content"><img class="plantuml" src="http://localhost:8080/png/U9npoazIqBLJ24uiIbImKl18pSd91m0rkGMq"></div></div>'
- doc = filter(input)
+ shared_examples_for 'renders correct markdown' do
+ it 'replaces plantuml pre tag with img tag' do
+ stub_application_setting(plantuml_enabled: true, plantuml_url: "http://localhost:8080")
- expect(doc.to_s).to eq output
+ input = if Feature.enabled?(:use_cmark_renderer)
+ '<pre lang="plantuml"><code>Bob -> Sara : Hello</code></pre>'
+ else
+ '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>'
+ end
+
+ output = '<div class="imageblock"><div class="content"><img class="plantuml" src="http://localhost:8080/png/U9npoazIqBLJ24uiIbImKl18pSd91m0rkGMq"></div></div>'
+ doc = filter(input)
+
+ expect(doc.to_s).to eq output
+ end
+
+ it 'does not replace plantuml pre tag with img tag if disabled' do
+ stub_application_setting(plantuml_enabled: false)
+
+ if Feature.enabled?(:use_cmark_renderer)
+ input = '<pre lang="plantuml"><code>Bob -> Sara : Hello</code></pre>'
+ output = '<pre lang="plantuml"><code>Bob -&gt; Sara : Hello</code></pre>'
+ else
+ input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>'
+ output = '<pre><code lang="plantuml">Bob -&gt; Sara : Hello</code></pre>'
+ end
+
+ doc = filter(input)
+
+ expect(doc.to_s).to eq output
+ end
+
+ it 'does not replace plantuml pre tag with img tag if url is invalid' do
+ stub_application_setting(plantuml_enabled: true, plantuml_url: "invalid")
+
+ input = if Feature.enabled?(:use_cmark_renderer)
+ '<pre lang="plantuml"><code>Bob -> Sara : Hello</code></pre>'
+ else
+ '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>'
+ end
+
+ output = '<div class="listingblock"><div class="content"><pre class="plantuml plantuml-error"> Error: cannot connect to PlantUML server at "invalid"</pre></div></div>'
+ doc = filter(input)
+
+ expect(doc.to_s).to eq output
+ end
end
- it 'does not replace plantuml pre tag with img tag if disabled' do
- stub_application_setting(plantuml_enabled: false)
- input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>'
- output = '<pre><code lang="plantuml">Bob -&gt; Sara : Hello</code></pre>'
- doc = filter(input)
+ context 'using ruby-based HTML renderer' do
+ before do
+ stub_feature_flags(use_cmark_renderer: false)
+ end
- expect(doc.to_s).to eq output
+ it_behaves_like 'renders correct markdown'
end
- it 'does not replace plantuml pre tag with img tag if url is invalid' do
- stub_application_setting(plantuml_enabled: true, plantuml_url: "invalid")
- input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>'
- output = '<div class="listingblock"><div class="content"><pre class="plantuml plantuml-error"> Error: cannot connect to PlantUML server at "invalid"</pre></div></div>'
- doc = filter(input)
+ context 'using c-based HTML renderer' do
+ before do
+ stub_feature_flags(use_cmark_renderer: true)
+ end
- expect(doc.to_s).to eq output
+ it_behaves_like 'renders correct markdown'
end
end
diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb
index f880fe06ce3..8eb8e5cf800 100644
--- a/spec/lib/banzai/filter/sanitization_filter_spec.rb
+++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb
@@ -45,10 +45,10 @@ RSpec.describe Banzai::Filter::SanitizationFilter do
it 'allows `text-align` property in `style` attribute on table elements' do
html = <<~HTML
- <table>
- <tr><th style="text-align: center">Head</th></tr>
- <tr><td style="text-align: right">Body</th></tr>
- </table>
+ <table>
+ <tr><th style="text-align: center">Head</th></tr>
+ <tr><td style="text-align: right">Body</th></tr>
+ </table>
HTML
doc = filter(html)
@@ -140,14 +140,14 @@ RSpec.describe Banzai::Filter::SanitizationFilter do
describe 'footnotes' do
it 'allows correct footnote id property on links' do
- exp = %q(<a href="#fn1" id="fnref1">foo/bar.md</a>)
+ exp = %q(<a href="#fn-first" id="fnref-first">foo/bar.md</a>)
act = filter(exp)
expect(act.to_html).to eq exp
end
it 'allows correct footnote id property on li element' do
- exp = %q(<ol><li id="fn1">footnote</li></ol>)
+ exp = %q(<ol><li id="fn-last">footnote</li></ol>)
act = filter(exp)
expect(act.to_html).to eq exp
@@ -156,7 +156,7 @@ RSpec.describe Banzai::Filter::SanitizationFilter do
it 'removes invalid id for footnote links' do
exp = %q(<a href="#fn1">link</a>)
- %w[fnrefx test xfnref1].each do |id|
+ %w[fnrefx test xfnref-1].each do |id|
act = filter(%(<a href="#fn1" id="#{id}">link</a>))
expect(act.to_html).to eq exp
@@ -166,18 +166,58 @@ RSpec.describe Banzai::Filter::SanitizationFilter do
it 'removes invalid id for footnote li' do
exp = %q(<ol><li>footnote</li></ol>)
- %w[fnx test xfn1].each do |id|
+ %w[fnx test xfn-1].each do |id|
act = filter(%(<ol><li id="#{id}">footnote</li></ol>))
expect(act.to_html).to eq exp
end
end
- it 'allows footnotes numbered higher than 9' do
- exp = %q(<a href="#fn15" id="fnref15">link</a><ol><li id="fn15">footnote</li></ol>)
- act = filter(exp)
+ context 'using ruby-based HTML renderer' do
+ before do
+ stub_feature_flags(use_cmark_renderer: false)
+ end
- expect(act.to_html).to eq exp
+ it 'allows correct footnote id property on links' do
+ exp = %q(<a href="#fn1" id="fnref1">foo/bar.md</a>)
+ act = filter(exp)
+
+ expect(act.to_html).to eq exp
+ end
+
+ it 'allows correct footnote id property on li element' do
+ exp = %q(<ol><li id="fn1">footnote</li></ol>)
+ act = filter(exp)
+
+ expect(act.to_html).to eq exp
+ end
+
+ it 'removes invalid id for footnote links' do
+ exp = %q(<a href="#fn1">link</a>)
+
+ %w[fnrefx test xfnref1].each do |id|
+ act = filter(%(<a href="#fn1" id="#{id}">link</a>))
+
+ expect(act.to_html).to eq exp
+ end
+ end
+
+ it 'removes invalid id for footnote li' do
+ exp = %q(<ol><li>footnote</li></ol>)
+
+ %w[fnx test xfn1].each do |id|
+ act = filter(%(<ol><li id="#{id}">footnote</li></ol>))
+
+ expect(act.to_html).to eq exp
+ end
+ end
+
+ it 'allows footnotes numbered higher than 9' do
+ exp = %q(<a href="#fn15" id="fnref15">link</a><ol><li id="fn15">footnote</li></ol>)
+ act = filter(exp)
+
+ expect(act.to_html).to eq exp
+ end
end
end
end
diff --git a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
index 7e45ecdd135..dfe022b51d2 100644
--- a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
+++ b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
@@ -11,130 +11,210 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
# after Markdown rendering.
result = filter(%{<pre lang="#{lang}"><code>&lt;script&gt;alert(1)&lt;/script&gt;</code></pre>})
- expect(result.to_html).not_to include("<script>alert(1)</script>")
- expect(result.to_html).to include("alert(1)")
+ # `(1)` symbols are wrapped by lexer tags.
+ expect(result.to_html).not_to match(%r{<script>alert.*<\/script>})
+
+ # `<>` stands for lexer tags like <span ...>, not &lt;s above.
+ expect(result.to_html).to match(%r{alert(<.*>)?\((<.*>)?1(<.*>)?\)})
end
end
- context "when no language is specified" do
- it "highlights as plaintext" do
- result = filter('<pre><code>def fun end</code></pre>')
+ shared_examples_for 'renders correct markdown' do
+ context "when no language is specified" do
+ it "highlights as plaintext" do
+ result = filter('<pre><code>def fun end</code></pre>')
- expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">def fun end</span></code></pre>')
- end
+ expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">def fun end</span></code></pre>')
+ end
- include_examples "XSS prevention", ""
- end
+ include_examples "XSS prevention", ""
+ end
- context "when contains mermaid diagrams" do
- it "ignores mermaid blocks" do
- result = filter('<pre data-mermaid-style="display"><code>mermaid code</code></pre>')
+ context "when contains mermaid diagrams" do
+ it "ignores mermaid blocks" do
+ result = filter('<pre data-mermaid-style="display"><code>mermaid code</code></pre>')
- expect(result.to_html).to eq('<pre data-mermaid-style="display"><code>mermaid code</code></pre>')
+ expect(result.to_html).to eq('<pre data-mermaid-style="display"><code>mermaid code</code></pre>')
+ end
end
- end
- context "when a valid language is specified" do
- it "highlights as that language" do
- result = filter('<pre><code lang="ruby">def fun end</code></pre>')
+ context "when a valid language is specified" do
+ it "highlights as that language" do
+ result = if Feature.enabled?(:use_cmark_renderer)
+ filter('<pre lang="ruby"><code>def fun end</code></pre>')
+ else
+ filter('<pre><code lang="ruby">def fun end</code></pre>')
+ end
+
+ expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight language-ruby" lang="ruby" v-pre="true"><code><span id="LC1" class="line" lang="ruby"><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></span></code></pre>')
+ end
- expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight language-ruby" lang="ruby" v-pre="true"><code><span id="LC1" class="line" lang="ruby"><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></span></code></pre>')
+ include_examples "XSS prevention", "ruby"
end
- include_examples "XSS prevention", "ruby"
- end
+ context "when an invalid language is specified" do
+ it "highlights as plaintext" do
+ result = if Feature.enabled?(:use_cmark_renderer)
+ filter('<pre lang="gnuplot"><code>This is a test</code></pre>')
+ else
+ filter('<pre><code lang="gnuplot">This is a test</code></pre>')
+ end
- context "when an invalid language is specified" do
- it "highlights as plaintext" do
- result = filter('<pre><code lang="gnuplot">This is a test</code></pre>')
+ expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">This is a test</span></code></pre>')
+ end
- expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">This is a test</span></code></pre>')
+ include_examples "XSS prevention", "gnuplot"
end
- include_examples "XSS prevention", "gnuplot"
- end
+ context "languages that should be passed through" do
+ let(:delimiter) { described_class::LANG_PARAMS_DELIMITER }
+ let(:data_attr) { described_class::LANG_PARAMS_ATTR }
- context "languages that should be passed through" do
- let(:delimiter) { described_class::PARAMS_DELIMITER }
- let(:data_attr) { described_class::LANG_PARAMS_ATTR }
+ %w(math mermaid plantuml suggestion).each do |lang|
+ context "when #{lang} is specified" do
+ it "highlights as plaintext but with the correct language attribute and class" do
+ result = if Feature.enabled?(:use_cmark_renderer)
+ filter(%{<pre lang="#{lang}"><code>This is a test</code></pre>})
+ else
+ filter(%{<pre><code lang="#{lang}">This is a test</code></pre>})
+ end
- %w(math mermaid plantuml suggestion).each do |lang|
- context "when #{lang} is specified" do
- it "highlights as plaintext but with the correct language attribute and class" do
- result = filter(%{<pre><code lang="#{lang}">This is a test</code></pre>})
+ expect(result.to_html).to eq(%{<pre class="code highlight js-syntax-highlight language-#{lang}" lang="#{lang}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre>})
+ end
- expect(result.to_html).to eq(%{<pre class="code highlight js-syntax-highlight language-#{lang}" lang="#{lang}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre>})
+ include_examples "XSS prevention", lang
end
- include_examples "XSS prevention", lang
+ context "when #{lang} has extra params" do
+ let(:lang_params) { 'foo-bar-kux' }
+
+ let(:xss_lang) do
+ if Feature.enabled?(:use_cmark_renderer)
+ "#{lang} data-meta=\"foo-bar-kux\"&lt;script&gt;alert(1)&lt;/script&gt;"
+ else
+ "#{lang}#{described_class::LANG_PARAMS_DELIMITER}&lt;script&gt;alert(1)&lt;/script&gt;"
+ end
+ end
+
+ it "includes data-lang-params tag with extra information" do
+ result = if Feature.enabled?(:use_cmark_renderer)
+ filter(%{<pre lang="#{lang}" data-meta="#{lang_params}"><code>This is a test</code></pre>})
+ else
+ filter(%{<pre><code lang="#{lang}#{delimiter}#{lang_params}">This is a test</code></pre>})
+ end
+
+ expect(result.to_html).to eq(%{<pre class="code highlight js-syntax-highlight language-#{lang}" lang="#{lang}" #{data_attr}="#{lang_params}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre>})
+ end
+
+ include_examples "XSS prevention", lang
+
+ if Feature.enabled?(:use_cmark_renderer)
+ include_examples "XSS prevention",
+ "#{lang} data-meta=\"foo-bar-kux\"&lt;script&gt;alert(1)&lt;/script&gt;"
+ else
+ include_examples "XSS prevention",
+ "#{lang}#{described_class::LANG_PARAMS_DELIMITER}&lt;script&gt;alert(1)&lt;/script&gt;"
+ end
+
+ include_examples "XSS prevention",
+ "#{lang} data-meta=\"foo-bar-kux\"<script>alert(1)</script>"
+ end
end
- context "when #{lang} has extra params" do
- let(:lang_params) { 'foo-bar-kux' }
+ context 'when multiple param delimiters are used' do
+ let(:lang) { 'suggestion' }
+ let(:lang_params) { '-1+10' }
- it "includes data-lang-params tag with extra information" do
- result = filter(%{<pre><code lang="#{lang}#{delimiter}#{lang_params}">This is a test</code></pre>})
+ let(:expected_result) do
+ %{<pre class="code highlight js-syntax-highlight language-#{lang}" lang="#{lang}" #{data_attr}="#{lang_params} more-things" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre>}
+ end
- expect(result.to_html).to eq(%{<pre class="code highlight js-syntax-highlight language-#{lang}" lang="#{lang}" #{data_attr}="#{lang_params}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre>})
+ context 'when delimiter is space' do
+ it 'delimits on the first appearance' do
+ if Feature.enabled?(:use_cmark_renderer)
+ result = filter(%{<pre lang="#{lang}" data-meta="#{lang_params} more-things"><code>This is a test</code></pre>})
+
+ expect(result.to_html).to eq(expected_result)
+ else
+ result = filter(%{<pre><code lang="#{lang}#{delimiter}#{lang_params}#{delimiter}more-things">This is a test</code></pre>})
+
+ expect(result.to_html).to eq(%{<pre class="code highlight js-syntax-highlight language-#{lang}" lang="#{lang}" #{data_attr}="#{lang_params}#{delimiter}more-things" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre>})
+ end
+ end
end
- include_examples "XSS prevention", lang
- include_examples "XSS prevention",
- "#{lang}#{described_class::PARAMS_DELIMITER}&lt;script&gt;alert(1)&lt;/script&gt;"
- include_examples "XSS prevention",
- "#{lang}#{described_class::PARAMS_DELIMITER}<script>alert(1)</script>"
+ context 'when delimiter is colon' do
+ it 'delimits on the first appearance' do
+ result = filter(%{<pre lang="#{lang}#{delimiter}#{lang_params} more-things"><code>This is a test</code></pre>})
+
+ if Feature.enabled?(:use_cmark_renderer)
+ expect(result.to_html).to eq(expected_result)
+ else
+ expect(result.to_html).to eq(%{<pre class=\"code highlight js-syntax-highlight language-plaintext\" lang=\"plaintext\" v-pre=\"true\"><code><span id=\"LC1\" class=\"line\" lang=\"plaintext\">This is a test</span></code></pre>})
+ end
+ end
+ end
end
end
- context 'when multiple param delimiters are used' do
- let(:lang) { 'suggestion' }
- let(:lang_params) { '-1+10' }
+ context "when sourcepos metadata is available" do
+ it "includes it in the highlighted code block" do
+ result = filter('<pre data-sourcepos="1:1-3:3"><code lang="plaintext">This is a test</code></pre>')
- it "delimits on the first appearance" do
- result = filter(%{<pre><code lang="#{lang}#{delimiter}#{lang_params}#{delimiter}more-things">This is a test</code></pre>})
-
- expect(result.to_html).to eq(%{<pre class="code highlight js-syntax-highlight language-#{lang}" lang="#{lang}" #{data_attr}="#{lang_params}#{delimiter}more-things" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre>})
+ expect(result.to_html).to eq('<pre data-sourcepos="1:1-3:3" class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">This is a test</span></code></pre>')
end
end
- end
- context "when sourcepos metadata is available" do
- it "includes it in the highlighted code block" do
- result = filter('<pre data-sourcepos="1:1-3:3"><code lang="plaintext">This is a test</code></pre>')
+ context "when Rouge lexing fails" do
+ before do
+ allow_next_instance_of(Rouge::Lexers::Ruby) do |instance|
+ allow(instance).to receive(:stream_tokens).and_raise(StandardError)
+ end
+ end
- expect(result.to_html).to eq('<pre data-sourcepos="1:1-3:3" class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">This is a test</span></code></pre>')
- end
- end
+ it "highlights as plaintext" do
+ result = if Feature.enabled?(:use_cmark_renderer)
+ filter('<pre lang="ruby"><code>This is a test</code></pre>')
+ else
+ filter('<pre><code lang="ruby">This is a test</code></pre>')
+ end
- context "when Rouge lexing fails" do
- before do
- allow_next_instance_of(Rouge::Lexers::Ruby) do |instance|
- allow(instance).to receive(:stream_tokens).and_raise(StandardError)
+ expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight" lang="" v-pre="true"><code><span id="LC1" class="line" lang="">This is a test</span></code></pre>')
end
+
+ include_examples "XSS prevention", "ruby"
end
- it "highlights as plaintext" do
- result = filter('<pre><code lang="ruby">This is a test</code></pre>')
+ context "when Rouge lexing fails after a retry" do
+ before do
+ allow_next_instance_of(Rouge::Lexers::PlainText) do |instance|
+ allow(instance).to receive(:stream_tokens).and_raise(StandardError)
+ end
+ end
- expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight" lang="" v-pre="true"><code><span id="LC1" class="line" lang="">This is a test</span></code></pre>')
- end
+ it "does not add highlighting classes" do
+ result = filter('<pre><code>This is a test</code></pre>')
+
+ expect(result.to_html).to eq('<pre><code>This is a test</code></pre>')
+ end
- include_examples "XSS prevention", "ruby"
+ include_examples "XSS prevention", "ruby"
+ end
end
- context "when Rouge lexing fails after a retry" do
+ context 'using ruby-based HTML renderer' do
before do
- allow_next_instance_of(Rouge::Lexers::PlainText) do |instance|
- allow(instance).to receive(:stream_tokens).and_raise(StandardError)
- end
+ stub_feature_flags(use_cmark_renderer: false)
end
- it "does not add highlighting classes" do
- result = filter('<pre><code>This is a test</code></pre>')
+ it_behaves_like 'renders correct markdown'
+ end
- expect(result.to_html).to eq('<pre><code>This is a test</code></pre>')
+ context 'using c-based HTML renderer' do
+ before do
+ stub_feature_flags(use_cmark_renderer: true)
end
- include_examples "XSS prevention", "ruby"
+ it_behaves_like 'renders correct markdown'
end
end
diff --git a/spec/lib/banzai/pipeline/emoji_pipeline_spec.rb b/spec/lib/banzai/pipeline/emoji_pipeline_spec.rb
index 6de9d65f1b2..8103846d4f7 100644
--- a/spec/lib/banzai/pipeline/emoji_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/emoji_pipeline_spec.rb
@@ -3,18 +3,20 @@
require 'spec_helper'
RSpec.describe Banzai::Pipeline::EmojiPipeline do
+ let(:emoji) { TanukiEmoji.find_by_alpha_code('100') }
+
def parse(text)
described_class.to_html(text, {})
end
it 'replaces emoji' do
- expected_result = "Hello world #{Gitlab::Emoji.gl_emoji_tag('100')}"
+ expected_result = "Hello world #{Gitlab::Emoji.gl_emoji_tag(emoji)}"
expect(parse('Hello world :100:')).to eq(expected_result)
end
it 'filters out HTML tags' do
- expected_result = "Hello &lt;b&gt;world&lt;/b&gt; #{Gitlab::Emoji.gl_emoji_tag('100')}"
+ expected_result = "Hello &lt;b&gt;world&lt;/b&gt; #{Gitlab::Emoji.gl_emoji_tag(emoji)}"
expect(parse('Hello <b>world</b> :100:')).to eq(expected_result)
end
diff --git a/spec/lib/banzai/pipeline/full_pipeline_spec.rb b/spec/lib/banzai/pipeline/full_pipeline_spec.rb
index 7a335fad3f8..01bca7b23e8 100644
--- a/spec/lib/banzai/pipeline/full_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/full_pipeline_spec.rb
@@ -31,29 +31,29 @@ RSpec.describe Banzai::Pipeline::FullPipeline do
describe 'footnotes' do
let(:project) { create(:project, :public) }
let(:html) { described_class.to_html(footnote_markdown, project: project) }
- let(:identifier) { html[/.*fnref1-(\d+).*/, 1] }
+ let(:identifier) { html[/.*fnref-1-(\d+).*/, 1] }
let(:footnote_markdown) do
<<~EOF
- first[^1] and second[^second] and twenty[^twenty]
+ first[^1] and second[^😄second] and twenty[^_twenty]
[^1]: one
- [^second]: two
- [^twenty]: twenty
+ [^😄second]: two
+ [^_twenty]: twenty
EOF
end
let(:filtered_footnote) do
- <<~EOF
- <p dir="auto">first<sup class="footnote-ref"><a href="#fn1-#{identifier}" id="fnref1-#{identifier}">1</a></sup> and second<sup class="footnote-ref"><a href="#fn2-#{identifier}" id="fnref2-#{identifier}">2</a></sup> and twenty<sup class="footnote-ref"><a href="#fn3-#{identifier}" id="fnref3-#{identifier}">3</a></sup></p>
+ <<~EOF.strip_heredoc
+ <p dir="auto">first<sup class="footnote-ref"><a href="#fn-1-#{identifier}" id="fnref-1-#{identifier}" data-footnote-ref="">1</a></sup> and second<sup class="footnote-ref"><a href="#fn-%F0%9F%98%84second-#{identifier}" id="fnref-%F0%9F%98%84second-#{identifier}" data-footnote-ref="">2</a></sup> and twenty<sup class="footnote-ref"><a href="#fn-_twenty-#{identifier}" id="fnref-_twenty-#{identifier}" data-footnote-ref="">3</a></sup></p>
- <section class="footnotes"><ol>
- <li id="fn1-#{identifier}">
- <p>one <a href="#fnref1-#{identifier}" class="footnote-backref"><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p>
+ <section class="footnotes" data-footnotes><ol>
+ <li id="fn-1-#{identifier}">
+ <p>one <a href="#fnref-1-#{identifier}" aria-label="Back to content" class="footnote-backref" data-footnote-backref=""><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p>
</li>
- <li id="fn2-#{identifier}">
- <p>two <a href="#fnref2-#{identifier}" class="footnote-backref"><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p>
+ <li id="fn-%F0%9F%98%84second-#{identifier}">
+ <p>two <a href="#fnref-%F0%9F%98%84second-#{identifier}" aria-label="Back to content" class="footnote-backref" data-footnote-backref=""><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p>
</li>
- <li id="fn3-#{identifier}">
- <p>twenty <a href="#fnref3-#{identifier}" class="footnote-backref"><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p>
+ <li id="fn-_twenty-#{identifier}">
+ <p>twenty <a href="#fnref-_twenty-#{identifier}" aria-label="Back to content" class="footnote-backref" data-footnote-backref=""><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p>
</li>
</ol></section>
EOF
@@ -64,6 +64,47 @@ RSpec.describe Banzai::Pipeline::FullPipeline do
expect(html.lines.map(&:strip).join("\n")).to eq filtered_footnote
end
+
+ context 'using ruby-based HTML renderer' do
+ let(:html) { described_class.to_html(footnote_markdown, project: project) }
+ let(:identifier) { html[/.*fnref1-(\d+).*/, 1] }
+ let(:footnote_markdown) do
+ <<~EOF
+ first[^1] and second[^second] and twenty[^twenty]
+ [^1]: one
+ [^second]: two
+ [^twenty]: twenty
+ EOF
+ end
+
+ let(:filtered_footnote) do
+ <<~EOF
+ <p dir="auto">first<sup class="footnote-ref"><a href="#fn1-#{identifier}" id="fnref1-#{identifier}">1</a></sup> and second<sup class="footnote-ref"><a href="#fn2-#{identifier}" id="fnref2-#{identifier}">2</a></sup> and twenty<sup class="footnote-ref"><a href="#fn3-#{identifier}" id="fnref3-#{identifier}">3</a></sup></p>
+
+ <section class="footnotes"><ol>
+ <li id="fn1-#{identifier}">
+ <p>one <a href="#fnref1-#{identifier}" class="footnote-backref"><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p>
+ </li>
+ <li id="fn2-#{identifier}">
+ <p>two <a href="#fnref2-#{identifier}" class="footnote-backref"><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p>
+ </li>
+ <li id="fn3-#{identifier}">
+ <p>twenty <a href="#fnref3-#{identifier}" class="footnote-backref"><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p>
+ </li>
+ </ol></section>
+ EOF
+ end
+
+ before do
+ stub_feature_flags(use_cmark_renderer: false)
+ end
+
+ it 'properly adds the necessary ids and classes' do
+ stub_commonmark_sourcepos_disabled
+
+ expect(html.lines.map(&:strip).join("\n")).to eq filtered_footnote
+ end
+ end
end
describe 'links are detected as malicious' do
diff --git a/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb b/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb
index 4903f624469..394fcc06eba 100644
--- a/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb
@@ -5,18 +5,7 @@ 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
-
+ shared_examples_for 'renders correct markdown' do
describe 'CommonMark tests', :aggregate_failures do
it 'converts all reference punctuation to literals' do
reference_chars = Banzai::Filter::MarkdownPreEscapeFilter::REFERENCE_CHARACTERS
@@ -32,7 +21,7 @@ RSpec.describe Banzai::Pipeline::PlainMarkdownPipeline do
expect(result[:escaped_literals]).to be_truthy
end
- it 'ensure we handle all the GitLab reference characters' do
+ it 'ensure we handle all the GitLab reference characters', :eager_load do
reference_chars = ObjectSpace.each_object(Class).map do |klass|
next unless klass.included_modules.include?(Referable)
next unless klass.respond_to?(:reference_prefix)
@@ -79,10 +68,19 @@ RSpec.describe Banzai::Pipeline::PlainMarkdownPipeline do
end
describe 'work in all other contexts, including URLs and link titles, link references, and info strings in fenced code blocks' do
+ let(:markdown) { %Q(``` foo\\@bar\nfoo\n```) }
+
+ it 'renders correct html' do
+ if Feature.enabled?(:use_cmark_renderer)
+ correct_html_included(markdown, %Q(<pre data-sourcepos="1:1-3:3" lang="foo@bar"><code>foo\n</code></pre>))
+ else
+ correct_html_included(markdown, %Q(<code lang="foo@bar">foo\n</code>))
+ end
+ end
+
where(:markdown, :expected) do
%q![foo](/bar\@ "\@title")! | %q(<a href="/bar@" title="@title">foo</a>)
%Q![foo]\n\n[foo]: /bar\\@ "\\@title"! | %q(<a href="/bar@" title="@title">foo</a>)
- %Q(``` foo\\@bar\nfoo\n```) | %Q(<code lang="foo@bar">foo\n</code>)
end
with_them do
@@ -91,4 +89,33 @@ RSpec.describe Banzai::Pipeline::PlainMarkdownPipeline do
end
end
end
+
+ 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 'using ruby-based HTML renderer' do
+ before do
+ stub_feature_flags(use_cmark_renderer: false)
+ end
+
+ it_behaves_like 'renders correct markdown'
+ end
+
+ context 'using c-based HTML renderer' do
+ before do
+ stub_feature_flags(use_cmark_renderer: true)
+ end
+
+ it_behaves_like 'renders correct markdown'
+ end
+ end
end
diff --git a/spec/lib/banzai/renderer_spec.rb b/spec/lib/banzai/renderer_spec.rb
index 52bf3087875..d487268da78 100644
--- a/spec/lib/banzai/renderer_spec.rb
+++ b/spec/lib/banzai/renderer_spec.rb
@@ -84,6 +84,24 @@ RSpec.describe Banzai::Renderer do
end
end
+ describe '#cacheless_render' do
+ context 'without cache' do
+ let(:object) { fake_object(fresh: false) }
+ let(:histogram) { double('prometheus histogram') }
+
+ it 'returns cacheless render field' do
+ allow(renderer).to receive(:render_result).and_return(output: 'test')
+ allow(renderer).to receive(:real_duration_histogram).and_return(histogram)
+ allow(renderer).to receive(:cpu_duration_histogram).and_return(histogram)
+
+ expect(renderer).to receive(:render_result).with('test', {})
+ expect(histogram).to receive(:observe).twice
+
+ renderer.cacheless_render('test')
+ end
+ end
+ end
+
describe '#post_process' do
let(:context_options) { {} }
let(:html) { 'Consequatur aperiam et nesciunt modi aut assumenda quo id. '}
diff --git a/spec/lib/bulk_imports/common/pipelines/milestones_pipeline_spec.rb b/spec/lib/bulk_imports/common/pipelines/milestones_pipeline_spec.rb
new file mode 100644
index 00000000000..9f71175f46f
--- /dev/null
+++ b/spec/lib/bulk_imports/common/pipelines/milestones_pipeline_spec.rb
@@ -0,0 +1,154 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Common::Pipelines::MilestonesPipeline do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:bulk_import) { create(:bulk_import, user: user) }
+ let(:tracker) { create(:bulk_import_tracker, entity: entity) }
+ let(:context) { BulkImports::Pipeline::Context.new(tracker) }
+ let(:source_project_id) { nil } # if set, then exported_milestone is a project milestone
+ let(:source_group_id) { nil } # if set, then exported_milestone is a group milestone
+ let(:exported_milestone_for_project) do
+ exported_milestone_for_group.merge(
+ 'events' => [{
+ 'project_id' => source_project_id,
+ 'author_id' => 9,
+ 'created_at' => "2021-08-12T19:12:49.810Z",
+ 'updated_at' => "2021-08-12T19:12:49.810Z",
+ 'target_type' => "Milestone",
+ 'group_id' => source_group_id,
+ 'fingerprint' => 'f270eb9b27d0',
+ 'id' => 66,
+ 'action' => "created"
+ }]
+ )
+ end
+
+ let(:exported_milestone_for_group) do
+ {
+ 'id' => 1,
+ 'title' => "v1.0",
+ 'project_id' => source_project_id,
+ 'description' => "Amet velit repellat ut rerum aut cum.",
+ 'due_date' => "2019-11-22",
+ 'created_at' => "2019-11-20T17:02:14.296Z",
+ 'updated_at' => "2019-11-20T17:02:14.296Z",
+ 'state' => "active",
+ 'iid' => 2,
+ 'start_date' => "2019-11-21",
+ 'group_id' => source_group_id
+ }
+ end
+
+ before do
+ group.add_owner(user)
+
+ allow_next_instance_of(BulkImports::Common::Extractors::NdjsonExtractor) do |extractor|
+ allow(extractor).to receive(:extract).and_return(BulkImports::Pipeline::ExtractedData.new(data: exported_milestones))
+ end
+ end
+
+ subject { described_class.new(context) }
+
+ shared_examples 'bulk_imports milestones pipeline' do
+ let(:tested_entity) { nil }
+
+ describe '#run' do
+ it 'imports milestones into destination' do
+ expect { subject.run }.to change(Milestone, :count).by(1)
+
+ imported_milestone = tested_entity.milestones.first
+
+ expect(imported_milestone.title).to eq("v1.0")
+ expect(imported_milestone.description).to eq("Amet velit repellat ut rerum aut cum.")
+ expect(imported_milestone.due_date.to_s).to eq("2019-11-22")
+ expect(imported_milestone.created_at).to eq("2019-11-20T17:02:14.296Z")
+ expect(imported_milestone.updated_at).to eq("2019-11-20T17:02:14.296Z")
+ expect(imported_milestone.start_date.to_s).to eq("2019-11-21")
+ end
+ end
+
+ describe '#load' do
+ context 'when milestone is not persisted' do
+ it 'saves the milestone' do
+ milestone = build(:milestone, group: group)
+
+ expect(milestone).to receive(:save!)
+
+ subject.load(context, milestone)
+ end
+ end
+
+ context 'when milestone is persisted' do
+ it 'does not save milestone' do
+ milestone = create(:milestone, group: group)
+
+ expect(milestone).not_to receive(:save!)
+
+ subject.load(context, milestone)
+ end
+ end
+
+ context 'when milestone is missing' do
+ it 'returns' do
+ expect(subject.load(context, nil)).to be_nil
+ end
+ end
+ end
+ end
+
+ context 'group milestone' do
+ let(:exported_milestones) { [[exported_milestone_for_group, 0]] }
+ let(:entity) do
+ create(
+ :bulk_import_entity,
+ group: group,
+ bulk_import: bulk_import,
+ source_full_path: 'source/full/path',
+ destination_name: 'My Destination Group',
+ destination_namespace: group.full_path
+ )
+ end
+
+ it_behaves_like 'bulk_imports milestones pipeline' do
+ let(:tested_entity) { group }
+ let(:source_group_id) { 1 }
+ end
+ end
+
+ context 'project milestone' do
+ let(:project) { create(:project, group: group) }
+ let(:exported_milestones) { [[exported_milestone_for_project, 0]] }
+
+ let(:entity) do
+ create(
+ :bulk_import_entity,
+ :project_entity,
+ project: project,
+ bulk_import: bulk_import,
+ source_full_path: 'source/full/path',
+ destination_name: 'My Destination Project',
+ destination_namespace: group.full_path
+ )
+ end
+
+ it_behaves_like 'bulk_imports milestones pipeline' do
+ let(:tested_entity) { project }
+ let(:source_project_id) { 1 }
+
+ it 'imports events' do
+ subject.run
+
+ imported_event = tested_entity.milestones.first.events.first
+
+ expect(imported_event.created_at).to eq("2021-08-12T19:12:49.810Z")
+ expect(imported_event.updated_at).to eq("2021-08-12T19:12:49.810Z")
+ expect(imported_event.target_type).to eq("Milestone")
+ expect(imported_event.fingerprint).to eq("f270eb9b27d0")
+ expect(imported_event.action).to eq("created")
+ end
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/common/pipelines/uploads_pipeline_spec.rb b/spec/lib/bulk_imports/common/pipelines/uploads_pipeline_spec.rb
new file mode 100644
index 00000000000..a3cc866a406
--- /dev/null
+++ b/spec/lib/bulk_imports/common/pipelines/uploads_pipeline_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Common::Pipelines::UploadsPipeline do
+ let_it_be(:tmpdir) { Dir.mktmpdir }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:entity) { create(:bulk_import_entity, :project_entity, project: project, source_full_path: 'test') }
+ let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) }
+ let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
+ let_it_be(:uploads_dir_path) { File.join(tmpdir, '72a497a02fe3ee09edae2ed06d390038') }
+ let_it_be(:upload_file_path) { File.join(uploads_dir_path, 'upload.txt')}
+
+ subject(:pipeline) { described_class.new(context) }
+
+ before do
+ stub_uploads_object_storage(FileUploader)
+
+ FileUtils.mkdir_p(uploads_dir_path)
+ FileUtils.touch(upload_file_path)
+ end
+
+ after do
+ FileUtils.remove_entry(tmpdir) if Dir.exist?(tmpdir)
+ end
+
+ describe '#run' do
+ it 'imports uploads into destination portable and removes tmpdir' do
+ allow(Dir).to receive(:mktmpdir).with('bulk_imports').and_return(tmpdir)
+ allow(pipeline).to receive(:extract).and_return(BulkImports::Pipeline::ExtractedData.new(data: [upload_file_path]))
+
+ pipeline.run
+
+ expect(project.uploads.map { |u| u.retrieve_uploader.filename }).to include('upload.txt')
+
+ expect(Dir.exist?(tmpdir)).to eq(false)
+ end
+ end
+
+ describe '#extract' do
+ it 'downloads & extracts upload paths' do
+ allow(Dir).to receive(:mktmpdir).and_return(tmpdir)
+ expect(pipeline).to receive(:untar_zxf)
+ file_download_service = instance_double("BulkImports::FileDownloadService")
+
+ expect(BulkImports::FileDownloadService)
+ .to receive(:new)
+ .with(
+ configuration: context.configuration,
+ relative_url: "/projects/test/export_relations/download?relation=uploads",
+ dir: tmpdir,
+ filename: 'uploads.tar.gz')
+ .and_return(file_download_service)
+
+ expect(file_download_service).to receive(:execute)
+
+ extracted_data = pipeline.extract(context)
+
+ expect(extracted_data.data).to contain_exactly(uploads_dir_path, upload_file_path)
+ end
+ end
+
+ describe '#load' do
+ it 'creates a file upload' do
+ expect { pipeline.load(context, upload_file_path) }.to change { project.uploads.count }.by(1)
+ end
+
+ context 'when dynamic path is nil' do
+ it 'returns' do
+ expect { pipeline.load(context, File.join(tmpdir, 'test')) }.not_to change { project.uploads.count }
+ end
+ end
+
+ context 'when path is a directory' do
+ it 'returns' do
+ expect { pipeline.load(context, uploads_dir_path) }.not_to change { project.uploads.count }
+ end
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/common/pipelines/wiki_pipeline_spec.rb b/spec/lib/bulk_imports/common/pipelines/wiki_pipeline_spec.rb
new file mode 100644
index 00000000000..0eefb7390dc
--- /dev/null
+++ b/spec/lib/bulk_imports/common/pipelines/wiki_pipeline_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Common::Pipelines::WikiPipeline do
+ describe '#run' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:bulk_import) { create(:bulk_import, user: user) }
+ let_it_be(:parent) { create(:project) }
+
+ let_it_be(:entity) do
+ create(
+ :bulk_import_entity,
+ :project_entity,
+ bulk_import: bulk_import,
+ source_full_path: 'source/full/path',
+ destination_name: 'My Destination Wiki',
+ destination_namespace: parent.full_path,
+ project: parent
+ )
+ end
+
+ it_behaves_like 'wiki pipeline imports a wiki for an entity'
+ end
+end
diff --git a/spec/lib/bulk_imports/groups/graphql/get_milestones_query_spec.rb b/spec/lib/bulk_imports/groups/graphql/get_milestones_query_spec.rb
deleted file mode 100644
index 7a0f964c5f3..00000000000
--- a/spec/lib/bulk_imports/groups/graphql/get_milestones_query_spec.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe BulkImports::Groups::Graphql::GetMilestonesQuery do
- it 'has a valid query' do
- tracker = create(:bulk_import_tracker)
- context = BulkImports::Pipeline::Context.new(tracker)
-
- query = GraphQL::Query.new(
- GitlabSchema,
- described_class.to_s,
- variables: described_class.variables(context)
- )
- result = GitlabSchema.static_validator.validate(query)
-
- expect(result[:errors]).to be_empty
- end
-
- describe '#data_path' do
- it 'returns data path' do
- expected = %w[data group milestones 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 milestones 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 de0b56045b3..69363bf0866 100644
--- a/spec/lib/bulk_imports/groups/loaders/group_loader_spec.rb
+++ b/spec/lib/bulk_imports/groups/loaders/group_loader_spec.rb
@@ -11,20 +11,66 @@ RSpec.describe BulkImports::Groups::Loaders::GroupLoader do
let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
let(:service_double) { instance_double(::Groups::CreateService) }
- let(:data) { { foo: :bar } }
+ let(:data) { { 'path' => 'test' } }
subject { described_class.new }
+ context 'when path is missing' do
+ it 'raises an error' do
+ expect { subject.load(context, {}) }.to raise_error(described_class::GroupCreationError, 'Path is missing')
+ end
+ end
+
+ context 'when destination namespace is not a group' do
+ it 'raises an error' do
+ entity.update!(destination_namespace: user.namespace.path)
+
+ expect { subject.load(context, data) }.to raise_error(described_class::GroupCreationError, 'Destination is not a group')
+ end
+ end
+
+ context 'when group exists' do
+ it 'raises an error' do
+ group1 = create(:group)
+ group2 = create(:group, parent: group1)
+ entity.update!(destination_namespace: group1.full_path)
+ data = { 'path' => group2.path }
+
+ expect { subject.load(context, data) }.to raise_error(described_class::GroupCreationError, 'Group exists')
+ end
+ end
+
+ context 'when there are other group errors' do
+ it 'raises an error with those errors' do
+ group = ::Group.new
+ group.validate
+ expected_errors = group.errors.full_messages.to_sentence
+
+ expect(::Groups::CreateService)
+ .to receive(:new)
+ .with(context.current_user, data)
+ .and_return(service_double)
+
+ expect(service_double).to receive(:execute).and_return(group)
+ expect(entity).not_to receive(:update!)
+
+ expect { subject.load(context, data) }.to raise_error(described_class::GroupCreationError, expected_errors)
+ end
+ end
+
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
+ group_double = instance_double(::Group)
+
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!)
+ expect(service_double).to receive(:execute).and_return(group_double)
+ expect(group_double).to receive(:errors).and_return([])
+ expect(entity).to receive(:update!).with(group: group_double)
subject.load(context, data)
end
@@ -40,7 +86,7 @@ RSpec.describe BulkImports::Groups::Loaders::GroupLoader do
context 'when there is parent group' do
let(:parent) { create(:group) }
- let(:data) { { 'parent_id' => parent.id } }
+ let(:data) { { 'parent_id' => parent.id, 'path' => 'test' } }
before do
allow(Ability).to receive(:allowed?).with(user, :create_subgroup, parent).and_return(true)
@@ -55,7 +101,7 @@ RSpec.describe BulkImports::Groups::Loaders::GroupLoader do
it 'does not create new group' do
expect(::Groups::CreateService).not_to receive(:new)
- subject.load(context, data)
+ expect { subject.load(context, data) }.to raise_error(described_class::GroupCreationError, 'User not allowed to create group')
end
end
@@ -69,7 +115,7 @@ RSpec.describe BulkImports::Groups::Loaders::GroupLoader do
context 'when there is parent group' do
let(:parent) { create(:group) }
- let(:data) { { 'parent_id' => parent.id } }
+ let(:data) { { 'parent_id' => parent.id, 'path' => 'test' } }
before do
allow(Ability).to receive(:allowed?).with(user, :create_subgroup, parent).and_return(false)
diff --git a/spec/lib/bulk_imports/groups/pipelines/milestones_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/milestones_pipeline_spec.rb
deleted file mode 100644
index a8354e62459..00000000000
--- a/spec/lib/bulk_imports/groups/pipelines/milestones_pipeline_spec.rb
+++ /dev/null
@@ -1,73 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe BulkImports::Groups::Pipelines::MilestonesPipeline do
- let_it_be(:user) { create(:user) }
- let_it_be(:group) { create(:group) }
- let_it_be(:bulk_import) { create(:bulk_import, user: user) }
- let_it_be(:filepath) { 'spec/fixtures/bulk_imports/gz/milestones.ndjson.gz' }
- let_it_be(:entity) do
- create(
- :bulk_import_entity,
- group: group,
- bulk_import: bulk_import,
- source_full_path: 'source/full/path',
- destination_name: 'My Destination Group',
- destination_namespace: group.full_path
- )
- end
-
- let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) }
- let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
-
- let(:tmpdir) { Dir.mktmpdir }
-
- before do
- FileUtils.copy_file(filepath, File.join(tmpdir, 'milestones.ndjson.gz'))
- group.add_owner(user)
- end
-
- subject { described_class.new(context) }
-
- describe '#run' do
- it 'imports group milestones into destination group and removes tmpdir' do
- allow(Dir).to receive(:mktmpdir).and_return(tmpdir)
- allow_next_instance_of(BulkImports::FileDownloadService) do |service|
- allow(service).to receive(:execute)
- end
-
- expect { subject.run }.to change(Milestone, :count).by(5)
- expect(group.milestones.pluck(:title)).to contain_exactly('v4.0', 'v3.0', 'v2.0', 'v1.0', 'v0.0')
- expect(File.directory?(tmpdir)).to eq(false)
- end
- end
-
- describe '#load' do
- context 'when milestone is not persisted' do
- it 'saves the milestone' do
- milestone = build(:milestone, group: group)
-
- expect(milestone).to receive(:save!)
-
- subject.load(context, milestone)
- end
- end
-
- context 'when milestone is persisted' do
- it 'does not save milestone' do
- milestone = create(:milestone, group: group)
-
- expect(milestone).not_to receive(:save!)
-
- subject.load(context, milestone)
- end
- end
-
- context 'when milestone is missing' do
- it 'returns' do
- expect(subject.load(context, nil)).to be_nil
- end
- end
- end
-end
diff --git a/spec/lib/bulk_imports/groups/stage_spec.rb b/spec/lib/bulk_imports/groups/stage_spec.rb
index b322b7b0edf..5719acac4d7 100644
--- a/spec/lib/bulk_imports/groups/stage_spec.rb
+++ b/spec/lib/bulk_imports/groups/stage_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe BulkImports::Groups::Stage do
[1, BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline],
[1, BulkImports::Groups::Pipelines::MembersPipeline],
[1, BulkImports::Common::Pipelines::LabelsPipeline],
- [1, BulkImports::Groups::Pipelines::MilestonesPipeline],
+ [1, BulkImports::Common::Pipelines::MilestonesPipeline],
[1, BulkImports::Groups::Pipelines::BadgesPipeline],
[2, BulkImports::Common::Pipelines::BoardsPipeline]
]
diff --git a/spec/lib/bulk_imports/ndjson_pipeline_spec.rb b/spec/lib/bulk_imports/ndjson_pipeline_spec.rb
index 7d156c2c3df..c5197fb29d9 100644
--- a/spec/lib/bulk_imports/ndjson_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/ndjson_pipeline_spec.rb
@@ -111,6 +111,7 @@ RSpec.describe BulkImports::NdjsonPipeline do
context = double(portable: group, current_user: user, import_export_config: config, bulk_import: import_double, entity: entity_double)
allow(subject).to receive(:import_export_config).and_return(config)
allow(subject).to receive(:context).and_return(context)
+ relation_object = double
expect(Gitlab::ImportExport::Group::RelationFactory)
.to receive(:create)
@@ -124,6 +125,8 @@ RSpec.describe BulkImports::NdjsonPipeline do
user: user,
excluded_keys: nil
)
+ .and_return(relation_object)
+ expect(relation_object).to receive(:assign_attributes).with(group: group)
subject.transform(context, data)
end
diff --git a/spec/lib/bulk_imports/projects/pipelines/external_pull_requests_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/external_pull_requests_pipeline_spec.rb
new file mode 100644
index 00000000000..8f610fcc2ae
--- /dev/null
+++ b/spec/lib/bulk_imports/projects/pipelines/external_pull_requests_pipeline_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Projects::Pipelines::ExternalPullRequestsPipeline do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:bulk_import) { create(:bulk_import) }
+ let_it_be(:entity) { create(:bulk_import_entity, :project_entity, project: project, bulk_import: bulk_import) }
+ let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) }
+ let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
+
+ let(:attributes) { {} }
+ let(:external_pr) { project.external_pull_requests.last }
+ let(:external_pull_request) do
+ {
+ 'pull_request_iid' => 4,
+ 'source_branch' => 'feature',
+ 'target_branch' => 'main',
+ 'source_repository' => 'repository',
+ 'target_repository' => 'repository',
+ 'source_sha' => 'abc',
+ 'target_sha' => 'xyz',
+ 'status' => 'open',
+ 'created_at' => '2019-12-24T14:04:50.053Z',
+ 'updated_at' => '2019-12-24T14:05:18.138Z'
+ }.merge(attributes)
+ end
+
+ subject(:pipeline) { described_class.new(context) }
+
+ describe '#run' do
+ before do
+ allow_next_instance_of(BulkImports::Common::Extractors::NdjsonExtractor) do |extractor|
+ allow(extractor).to receive(:remove_tmp_dir)
+ allow(extractor).to receive(:extract).and_return(BulkImports::Pipeline::ExtractedData.new(data: [[external_pull_request, 0]]))
+ end
+
+ pipeline.run
+ end
+
+ it 'imports external pull request', :aggregate_failures do
+ expect(external_pr.pull_request_iid).to eq(external_pull_request['pull_request_iid'])
+ expect(external_pr.source_branch).to eq(external_pull_request['source_branch'])
+ expect(external_pr.target_branch).to eq(external_pull_request['target_branch'])
+ expect(external_pr.status).to eq(external_pull_request['status'])
+ expect(external_pr.created_at).to eq(external_pull_request['created_at'])
+ expect(external_pr.updated_at).to eq(external_pull_request['updated_at'])
+ end
+
+ context 'when status is closed' do
+ let(:attributes) { { 'status' => 'closed' } }
+
+ it 'imports closed external pull request' do
+ expect(external_pr.status).to eq(attributes['status'])
+ end
+ end
+
+ context 'when from fork' do
+ let(:attributes) { { 'source_repository' => 'source' } }
+
+ it 'does not create external pull request' do
+ expect(external_pr).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb
new file mode 100644
index 00000000000..3f02356b41e
--- /dev/null
+++ b/spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb
@@ -0,0 +1,297 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Projects::Pipelines::MergeRequestsPipeline do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :repository, group: group) }
+ let_it_be(:bulk_import) { create(:bulk_import, user: user) }
+ let_it_be(:entity) do
+ create(
+ :bulk_import_entity,
+ :project_entity,
+ project: project,
+ bulk_import: bulk_import,
+ source_full_path: 'source/full/path',
+ destination_name: 'My Destination Project',
+ destination_namespace: group.full_path
+ )
+ end
+
+ let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) }
+ let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
+
+ let(:mr) do
+ {
+ 'iid' => 7,
+ 'author_id' => 22,
+ 'source_project_id' => 1234,
+ 'target_project_id' => 1234,
+ 'title' => 'Imported MR',
+ 'description' => 'Description',
+ 'state' => 'opened',
+ 'source_branch' => 'feature',
+ 'target_branch' => 'main',
+ 'source_branch_sha' => 'ABCD',
+ 'target_branch_sha' => 'DCBA',
+ 'created_at' => '2020-06-14T15:02:47.967Z',
+ 'updated_at' => '2020-06-14T15:03:47.967Z',
+ 'merge_request_diff' => {
+ 'state' => 'collected',
+ 'base_commit_sha' => 'ae73cb07c9eeaf35924a10f713b364d32b2dd34f',
+ 'head_commit_sha' => 'a97f74ddaa848b707bea65441c903ae4bf5d844d',
+ 'start_commit_sha' => '9eea46b5c72ead701c22f516474b95049c9d9462',
+ 'merge_request_diff_commits' => [
+ {
+ 'sha' => 'COMMIT1',
+ 'relative_order' => 0,
+ 'message' => 'commit message',
+ 'authored_date' => '2014-08-06T08:35:52.000+02:00',
+ 'committed_date' => '2014-08-06T08:35:52.000+02:00',
+ 'commit_author' => {
+ 'name' => 'Commit Author',
+ 'email' => 'gitlab@example.com'
+ },
+ 'committer' => {
+ 'name' => 'Committer',
+ 'email' => 'committer@example.com'
+ }
+ }
+ ],
+ 'merge_request_diff_files' => [
+ {
+ 'relative_order' => 0,
+ 'utf8_diff' => '--- a/.gitignore\n+++ b/.gitignore\n@@ -1 +1 @@ test\n',
+ 'new_path' => '.gitignore',
+ 'old_path' => '.gitignore',
+ 'a_mode' => '100644',
+ 'b_mode' => '100644',
+ 'new_file' => false,
+ 'renamed_file' => false,
+ 'deleted_file' => false,
+ 'too_large' => false
+ }
+ ]
+ }
+ }.merge(attributes)
+ end
+
+ let(:attributes) { {} }
+ let(:imported_mr) { project.merge_requests.find_by_title(mr['title']) }
+
+ subject(:pipeline) { described_class.new(context) }
+
+ describe '#run' do
+ before do
+ group.add_owner(user)
+
+ allow_next_instance_of(BulkImports::Common::Extractors::NdjsonExtractor) do |extractor|
+ allow(extractor).to receive(:remove_tmp_dir)
+ allow(extractor).to receive(:extract).and_return(BulkImports::Pipeline::ExtractedData.new(data: [[mr, 0]]))
+ end
+
+ allow(project.repository).to receive(:fetch_source_branch!).and_return(true)
+ allow(project.repository).to receive(:branch_exists?).and_return(false)
+ allow(project.repository).to receive(:create_branch)
+
+ pipeline.run
+ end
+
+ it 'imports a merge request' do
+ expect(project.merge_requests.count).to eq(1)
+ expect(imported_mr.title).to eq(mr['title'])
+ expect(imported_mr.description).to eq(mr['description'])
+ expect(imported_mr.state).to eq(mr['state'])
+ expect(imported_mr.iid).to eq(mr['iid'])
+ expect(imported_mr.created_at).to eq(mr['created_at'])
+ expect(imported_mr.updated_at).to eq(mr['updated_at'])
+ expect(imported_mr.author).to eq(user)
+ end
+
+ context 'merge request state' do
+ context 'when mr is closed' do
+ let(:attributes) { { 'state' => 'closed' } }
+
+ it 'imported mr as closed' do
+ expect(imported_mr.state).to eq(attributes['state'])
+ end
+ end
+
+ context 'when mr is merged' do
+ let(:attributes) { { 'state' => 'merged' } }
+
+ it 'imported mr as merged' do
+ expect(imported_mr.state).to eq(attributes['state'])
+ end
+ end
+ end
+
+ context 'source & target project' do
+ it 'has the new project as target' do
+ expect(imported_mr.target_project).to eq(project)
+ end
+
+ it 'has the new project as source' do
+ expect(imported_mr.source_project).to eq(project)
+ end
+
+ context 'when source/target projects differ' do
+ let(:attributes) { { 'source_project_id' => 4321 } }
+
+ it 'has no source' do
+ expect(imported_mr.source_project).to be_nil
+ end
+
+ context 'when diff_head_sha is present' do
+ let(:attributes) { { 'diff_head_sha' => 'HEAD', 'source_project_id' => 4321 } }
+
+ it 'has the new project as source' do
+ expect(imported_mr.source_project).to eq(project)
+ end
+ end
+ end
+ end
+
+ context 'resource label events' do
+ let(:attributes) { { 'resource_label_events' => [{ 'action' => 'add', 'user_id' => 1 }] } }
+
+ it 'restores resource label events' do
+ expect(imported_mr.resource_label_events.first.action).to eq('add')
+ end
+ end
+
+ context 'award emoji' do
+ let(:attributes) { { 'award_emoji' => [{ 'name' => 'tada', 'user_id' => 22 }] } }
+
+ it 'has award emoji' do
+ expect(imported_mr.award_emoji.first.name).to eq(attributes['award_emoji'].first['name'])
+ end
+ end
+
+ context 'notes' do
+ let(:note) { imported_mr.notes.first }
+ let(:attributes) do
+ {
+ 'notes' => [
+ {
+ 'note' => 'Issue note',
+ 'note_html' => '<p>something else entirely</p>',
+ 'cached_markdown_version' => 917504,
+ 'author_id' => 22,
+ 'author' => { 'name' => 'User 22' },
+ 'created_at' => '2016-06-14T15:02:56.632Z',
+ 'updated_at' => '2016-06-14T15:02:47.770Z',
+ 'award_emoji' => [{ 'name' => 'clapper', 'user_id' => 22 }]
+ }
+ ]
+ }
+ end
+
+ it 'imports mr note' do
+ expect(note).to be_present
+ expect(note.note).to include('By User 22')
+ expect(note.note).to include(attributes['notes'].first['note'])
+ expect(note.author).to eq(user)
+ end
+
+ it 'has award emoji' do
+ emoji = note.award_emoji.first
+
+ expect(emoji.name).to eq('clapper')
+ expect(emoji.user).to eq(user)
+ end
+
+ it 'does not import note_html' do
+ expect(note.note_html).to match(attributes['notes'].first['note'])
+ expect(note.note_html).not_to match(attributes['notes'].first['note_html'])
+ end
+ end
+
+ context 'system note metadata' do
+ let(:attributes) do
+ {
+ 'notes' => [
+ {
+ 'note' => 'added 3 commits',
+ 'system' => true,
+ 'author_id' => 22,
+ 'author' => { 'name' => 'User 22' },
+ 'created_at' => '2016-06-14T15:02:56.632Z',
+ 'updated_at' => '2016-06-14T15:02:47.770Z',
+ 'system_note_metadata' => { 'action' => 'commit', 'commit_count' => 3 }
+ }
+ ]
+ }
+ end
+
+ it 'restores system note metadata' do
+ note = imported_mr.notes.first
+
+ expect(note.system).to eq(true)
+ expect(note.noteable_type).to eq('MergeRequest')
+ expect(note.system_note_metadata.action).to eq('commit')
+ expect(note.system_note_metadata.commit_count).to eq(3)
+ end
+ end
+
+ context 'diffs' do
+ it 'imports merge request diff' do
+ expect(imported_mr.merge_request_diff).to be_present
+ end
+
+ it 'has the correct data for merge request latest_merge_request_diff' do
+ expect(imported_mr.latest_merge_request_diff_id).to eq(imported_mr.merge_request_diffs.maximum(:id))
+ end
+
+ it 'imports diff files' do
+ expect(imported_mr.merge_request_diff.merge_request_diff_files.count).to eq(1)
+ end
+
+ context 'diff commits' do
+ it 'imports diff commits' do
+ expect(imported_mr.merge_request_diff.merge_request_diff_commits.count).to eq(1)
+ end
+
+ it 'assigns committer and author details to diff commits' do
+ commit = imported_mr.merge_request_diff.merge_request_diff_commits.first
+
+ expect(commit.commit_author_id).not_to be_nil
+ expect(commit.committer_id).not_to be_nil
+ end
+
+ it 'assigns the correct commit users to diff commits' do
+ commit = MergeRequestDiffCommit.find_by(sha: 'COMMIT1')
+
+ expect(commit.commit_author.name).to eq('Commit Author')
+ expect(commit.commit_author.email).to eq('gitlab@example.com')
+ expect(commit.committer.name).to eq('Committer')
+ expect(commit.committer.email).to eq('committer@example.com')
+ end
+ end
+ end
+
+ context 'labels' do
+ let(:attributes) do
+ {
+ 'label_links' => [
+ { 'label' => { 'title' => 'imported label 1', 'type' => 'ProjectLabel' } },
+ { 'label' => { 'title' => 'imported label 2', 'type' => 'ProjectLabel' } }
+ ]
+ }
+ end
+
+ it 'imports labels' do
+ expect(imported_mr.labels.pluck(:title)).to contain_exactly('imported label 1', 'imported label 2')
+ end
+ end
+
+ context 'milestone' do
+ let(:attributes) { { 'milestone' => { 'title' => 'imported milestone' } } }
+
+ it 'imports milestone' do
+ expect(imported_mr.milestone.title).to eq(attributes.dig('milestone', 'title'))
+ end
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/projects/pipelines/protected_branches_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/protected_branches_pipeline_spec.rb
new file mode 100644
index 00000000000..7de2e266192
--- /dev/null
+++ b/spec/lib/bulk_imports/projects/pipelines/protected_branches_pipeline_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Projects::Pipelines::ProtectedBranchesPipeline do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:bulk_import) { create(:bulk_import, user: user) }
+ let_it_be(:entity) { create(:bulk_import_entity, :project_entity, project: project, bulk_import: bulk_import) }
+ let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) }
+ let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
+ let_it_be(:protected_branch) do
+ {
+ 'name' => 'main',
+ 'created_at' => '2016-06-14T15:02:47.967Z',
+ 'updated_at' => '2016-06-14T15:02:47.967Z',
+ 'merge_access_levels' => [
+ {
+ 'access_level' => 40,
+ 'created_at' => '2016-06-15T15:02:47.967Z',
+ 'updated_at' => '2016-06-15T15:02:47.967Z'
+ }
+ ],
+ 'push_access_levels' => [
+ {
+ 'access_level' => 30,
+ 'created_at' => '2016-06-16T15:02:47.967Z',
+ 'updated_at' => '2016-06-16T15:02:47.967Z'
+ }
+ ]
+ }
+ end
+
+ subject(:pipeline) { described_class.new(context) }
+
+ describe '#run' do
+ it 'imports protected branch information' do
+ allow_next_instance_of(BulkImports::Common::Extractors::NdjsonExtractor) do |extractor|
+ allow(extractor).to receive(:extract).and_return(BulkImports::Pipeline::ExtractedData.new(data: [protected_branch, 0]))
+ end
+
+ pipeline.run
+
+ imported_protected_branch = project.protected_branches.last
+ merge_access_level = imported_protected_branch.merge_access_levels.first
+ push_access_level = imported_protected_branch.push_access_levels.first
+
+ aggregate_failures do
+ expect(imported_protected_branch.name).to eq(protected_branch['name'])
+ expect(imported_protected_branch.updated_at).to eq(protected_branch['updated_at'])
+ expect(imported_protected_branch.created_at).to eq(protected_branch['created_at'])
+ expect(merge_access_level.access_level).to eq(protected_branch['merge_access_levels'].first['access_level'])
+ expect(merge_access_level.created_at).to eq(protected_branch['merge_access_levels'].first['created_at'])
+ expect(merge_access_level.updated_at).to eq(protected_branch['merge_access_levels'].first['updated_at'])
+ expect(push_access_level.access_level).to eq(protected_branch['push_access_levels'].first['access_level'])
+ expect(push_access_level.created_at).to eq(protected_branch['push_access_levels'].first['created_at'])
+ expect(push_access_level.updated_at).to eq(protected_branch['push_access_levels'].first['updated_at'])
+ end
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/projects/pipelines/repository_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/repository_pipeline_spec.rb
index af39ec7a11c..583485faf8d 100644
--- a/spec/lib/bulk_imports/projects/pipelines/repository_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/projects/pipelines/repository_pipeline_spec.rb
@@ -3,71 +3,72 @@
require 'spec_helper'
RSpec.describe BulkImports::Projects::Pipelines::RepositoryPipeline do
- describe '#run' do
- let_it_be(:user) { create(:user) }
- let_it_be(:parent) { create(:project) }
- let_it_be(:bulk_import) { create(:bulk_import, user: user) }
- let_it_be(:bulk_import_configuration) { create(:bulk_import_configuration, bulk_import: bulk_import) }
-
- let_it_be(:entity) do
- create(
- :bulk_import_entity,
- :project_entity,
- bulk_import: bulk_import,
- source_full_path: 'source/full/path',
- destination_name: 'My Destination Repository',
- destination_namespace: parent.full_path,
- project: parent
- )
- end
+ let_it_be(:user) { create(:user) }
+ let_it_be(:parent) { create(:project) }
+ let_it_be(:bulk_import) { create(:bulk_import, user: user) }
+ let_it_be(:bulk_import_configuration) { create(:bulk_import_configuration, bulk_import: bulk_import) }
+
+ let_it_be(:entity) do
+ create(
+ :bulk_import_entity,
+ :project_entity,
+ bulk_import: bulk_import,
+ source_full_path: 'source/full/path',
+ destination_name: 'My Destination Repository',
+ destination_namespace: parent.full_path,
+ project: parent
+ )
+ end
- let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) }
- let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
+ let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) }
+ let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
- context 'successfully imports repository' do
- let(:project_data) do
- {
- 'httpUrlToRepo' => 'http://test.git'
- }
- end
+ let(:extracted_data) { BulkImports::Pipeline::ExtractedData.new(data: project_data) }
- subject { described_class.new(context) }
+ subject(:pipeline) { described_class.new(context) }
+
+ before do
+ allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor|
+ allow(extractor).to receive(:extract).and_return(extracted_data)
+ end
+ end
+
+ describe '#run' do
+ context 'successfully imports repository' do
+ let(:project_data) { { 'httpUrlToRepo' => 'http://test.git' } }
it 'imports new repository into destination project' do
- allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor|
- allow(extractor).to receive(:extract).and_return(BulkImports::Pipeline::ExtractedData.new(data: project_data))
- end
+ url = project_data['httpUrlToRepo'].sub("://", "://oauth2:#{bulk_import_configuration.access_token}@")
- expect_next_instance_of(Gitlab::GitalyClient::RepositoryService) do |repository_service|
- url = project_data['httpUrlToRepo'].sub("://", "://oauth2:#{bulk_import_configuration.access_token}@")
- expect(repository_service).to receive(:import_repository).with(url).and_return 0
- end
+ expect(context.portable).to receive(:ensure_repository)
+ expect(context.portable.repository).to receive(:fetch_as_mirror).with(url)
- subject.run
+ pipeline.run
end
end
context 'blocked local networks' do
- let(:project_data) do
- {
- 'httpUrlToRepo' => 'http://localhost/foo.git'
- }
- end
+ let(:project_data) { { 'httpUrlToRepo' => 'http://localhost/foo.git' } }
- before do
+ it 'imports new repository into destination project' do
allow(Gitlab.config.gitlab).to receive(:host).and_return('notlocalhost.gitlab.com')
allow(Gitlab::CurrentSettings).to receive(:allow_local_requests_from_web_hooks_and_services?).and_return(false)
- allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor|
- allow(extractor).to receive(:extract).and_return(BulkImports::Pipeline::ExtractedData.new(data: project_data))
- end
- end
- subject { described_class.new(context) }
+ pipeline.run
- it 'imports new repository into destination project' do
- subject.run
- expect(context.entity.failed?).to be_truthy
+ expect(context.entity.failed?).to eq(true)
end
end
end
+
+ describe '#after_run' do
+ it 'executes housekeeping service after import' do
+ service = instance_double(Repositories::HousekeepingService)
+
+ expect(Repositories::HousekeepingService).to receive(:new).with(context.portable, :gc).and_return(service)
+ expect(service).to receive(:execute)
+
+ pipeline.after_run(context)
+ end
+ end
end
diff --git a/spec/lib/bulk_imports/projects/stage_spec.rb b/spec/lib/bulk_imports/projects/stage_spec.rb
index c606cf7c556..e7670085f60 100644
--- a/spec/lib/bulk_imports/projects/stage_spec.rb
+++ b/spec/lib/bulk_imports/projects/stage_spec.rb
@@ -8,9 +8,15 @@ RSpec.describe BulkImports::Projects::Stage do
[0, BulkImports::Projects::Pipelines::ProjectPipeline],
[1, BulkImports::Projects::Pipelines::RepositoryPipeline],
[2, BulkImports::Common::Pipelines::LabelsPipeline],
+ [2, BulkImports::Common::Pipelines::MilestonesPipeline],
[3, BulkImports::Projects::Pipelines::IssuesPipeline],
[4, BulkImports::Common::Pipelines::BoardsPipeline],
- [5, BulkImports::Common::Pipelines::EntityFinisher]
+ [4, BulkImports::Projects::Pipelines::MergeRequestsPipeline],
+ [4, BulkImports::Projects::Pipelines::ExternalPullRequestsPipeline],
+ [4, BulkImports::Projects::Pipelines::ProtectedBranchesPipeline],
+ [5, BulkImports::Common::Pipelines::WikiPipeline],
+ [5, BulkImports::Common::Pipelines::UploadsPipeline],
+ [6, BulkImports::Common::Pipelines::EntityFinisher]
]
end
@@ -22,7 +28,8 @@ RSpec.describe BulkImports::Projects::Stage do
describe '#pipelines' do
it 'list all the pipelines with their stage number, ordered by stage' do
- expect(subject.pipelines).to eq(pipelines)
+ expect(subject.pipelines & pipelines).to contain_exactly(*pipelines)
+ expect(subject.pipelines.last.last).to eq(BulkImports::Common::Pipelines::EntityFinisher)
end
end
end
diff --git a/spec/lib/container_registry/client_spec.rb b/spec/lib/container_registry/client_spec.rb
index 47a8fcf5dd0..259d7d5ad13 100644
--- a/spec/lib/container_registry/client_spec.rb
+++ b/spec/lib/container_registry/client_spec.rb
@@ -279,7 +279,7 @@ RSpec.describe ContainerRegistry::Client do
it 'uploads the manifest and returns the digest' do
stub_request(:put, "http://container-registry/v2/path/manifests/tagA")
.with(body: "{\n \"foo\": \"bar\"\n}", headers: manifest_headers)
- .to_return(status: 200, body: "", headers: { 'docker-content-digest' => 'sha256:123' })
+ .to_return(status: 200, body: "", headers: { DependencyProxy::Manifest::DIGEST_HEADER => 'sha256:123' })
expect_new_faraday(timeout: false)
diff --git a/spec/lib/container_registry/tag_spec.rb b/spec/lib/container_registry/tag_spec.rb
index d6e6b254dd9..9b931ab6dbc 100644
--- a/spec/lib/container_registry/tag_spec.rb
+++ b/spec/lib/container_registry/tag_spec.rb
@@ -213,7 +213,7 @@ RSpec.describe ContainerRegistry::Tag do
before do
stub_request(:head, 'http://registry.gitlab/v2/group/test/manifests/tag')
.with(headers: headers)
- .to_return(status: 200, headers: { 'Docker-Content-Digest' => 'sha256:digest' })
+ .to_return(status: 200, headers: { DependencyProxy::Manifest::DIGEST_HEADER => 'sha256:digest' })
end
describe '#digest' do
diff --git a/spec/lib/error_tracking/collector/payload_validator_spec.rb b/spec/lib/error_tracking/collector/payload_validator_spec.rb
new file mode 100644
index 00000000000..852cf9eac6c
--- /dev/null
+++ b/spec/lib/error_tracking/collector/payload_validator_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ErrorTracking::Collector::PayloadValidator do
+ describe '#valid?' do
+ RSpec.shared_examples 'valid payload' do
+ it 'returns true' do
+ expect(described_class.new.valid?(payload)).to be_truthy
+ end
+ end
+
+ RSpec.shared_examples 'invalid payload' do
+ it 'returns false' do
+ expect(described_class.new.valid?(payload)).to be_falsey
+ end
+ end
+
+ context 'ruby payload' do
+ let(:payload) { Gitlab::Json.parse(fixture_file('error_tracking/parsed_event.json')) }
+
+ it_behaves_like 'valid payload'
+ end
+
+ context 'python payload' do
+ let(:payload) { Gitlab::Json.parse(fixture_file('error_tracking/python_event.json')) }
+
+ it_behaves_like 'valid payload'
+ end
+
+ context 'browser payload' do
+ let(:payload) { Gitlab::Json.parse(fixture_file('error_tracking/browser_event.json')) }
+
+ it_behaves_like 'valid payload'
+ end
+
+ context 'empty payload' do
+ let(:payload) { '' }
+
+ it_behaves_like 'invalid payload'
+ end
+
+ context 'invalid payload' do
+ let(:payload) { { 'foo' => 'bar' } }
+
+ it_behaves_like 'invalid payload'
+ end
+ end
+end
diff --git a/spec/lib/error_tracking/collector/sentry_request_parser_spec.rb b/spec/lib/error_tracking/collector/sentry_request_parser_spec.rb
index 6f12c6d25e0..06f4b64ce93 100644
--- a/spec/lib/error_tracking/collector/sentry_request_parser_spec.rb
+++ b/spec/lib/error_tracking/collector/sentry_request_parser_spec.rb
@@ -33,12 +33,5 @@ RSpec.describe ErrorTracking::Collector::SentryRequestParser do
context 'plain text sentry request' do
it_behaves_like 'valid parser'
end
-
- context 'gzip encoded sentry request' do
- let(:headers) { { 'Content-Encoding' => 'gzip' } }
- let(:body) { Zlib.gzip(raw_event) }
-
- it_behaves_like 'valid parser'
- end
end
end
diff --git a/spec/lib/feature/gitaly_spec.rb b/spec/lib/feature/gitaly_spec.rb
index 311589c3253..ed80e31e3cd 100644
--- a/spec/lib/feature/gitaly_spec.rb
+++ b/spec/lib/feature/gitaly_spec.rb
@@ -78,7 +78,9 @@ RSpec.describe Feature::Gitaly do
context 'when table does not exist' do
before do
- allow(::Gitlab::Database.main).to receive(:cached_table_exists?).and_return(false)
+ allow(Feature::FlipperFeature.database)
+ .to receive(:cached_table_exists?)
+ .and_return(false)
end
it 'returns an empty Hash' do
diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb
index 9d4820f9a4c..58e7292c125 100644
--- a/spec/lib/feature_spec.rb
+++ b/spec/lib/feature_spec.rb
@@ -102,12 +102,14 @@ RSpec.describe Feature, stub_feature_flags: false do
describe '.flipper' do
context 'when request store is inactive' do
- it 'memoizes the Flipper instance' do
+ it 'memoizes the Flipper instance but does not not enable Flipper memoization' do
expect(Flipper).to receive(:new).once.and_call_original
2.times do
- described_class.send(:flipper)
+ described_class.flipper
end
+
+ expect(described_class.flipper.adapter.memoizing?).to eq(false)
end
end
@@ -115,9 +117,11 @@ RSpec.describe Feature, stub_feature_flags: false do
it 'memoizes the Flipper instance' do
expect(Flipper).to receive(:new).once.and_call_original
- described_class.send(:flipper)
+ described_class.flipper
described_class.instance_variable_set(:@flipper, nil)
- described_class.send(:flipper)
+ described_class.flipper
+
+ expect(described_class.flipper.adapter.memoizing?).to eq(true)
end
end
end
@@ -310,7 +314,7 @@ RSpec.describe Feature, stub_feature_flags: false do
context 'when database exists' do
before do
- allow(Gitlab::Database.main).to receive(:exists?).and_return(true)
+ allow(ApplicationRecord.database).to receive(:exists?).and_return(true)
end
it 'checks the persisted status and returns false' do
@@ -322,7 +326,7 @@ RSpec.describe Feature, stub_feature_flags: false do
context 'when database does not exist' do
before do
- allow(Gitlab::Database.main).to receive(:exists?).and_return(false)
+ allow(ApplicationRecord.database).to receive(:exists?).and_return(false)
end
it 'returns false without checking the status in the database' do
diff --git a/spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb b/spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb
index 05833cf4ec4..b67425ae012 100644
--- a/spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb
+++ b/spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb
@@ -99,4 +99,15 @@ RSpec.describe Gitlab::UsageMetricDefinitionGenerator, :silence_stdout do
expect(YAML.safe_load(File.read(metric_definition_path))).to include("name" => "some name")
end
end
+
+ context 'with multiple file names' do
+ let(:key_paths) { ['counts_weekly.test_metric', 'counts_weekly.test1_metric'] }
+
+ it 'creates multiple files' do
+ described_class.new(key_paths, { 'dir' => dir }).invoke_all
+ files = Dir.glob(File.join(temp_dir, 'metrics/counts_7d/*_metric.yml'))
+
+ expect(files.count).to eq(2)
+ end
+ end
end
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/aggregated/base_query_builder_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/aggregated/base_query_builder_spec.rb
new file mode 100644
index 00000000000..bf2f8d8159b
--- /dev/null
+++ b/spec/lib/gitlab/analytics/cycle_analytics/aggregated/base_query_builder_spec.rb
@@ -0,0 +1,150 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Analytics::CycleAnalytics::Aggregated::BaseQueryBuilder do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:milestone) { create(:milestone, project: project) }
+ let_it_be(:user_1) { create(:user) }
+
+ let_it_be(:label_1) { create(:label, project: project) }
+ let_it_be(:label_2) { create(:label, project: project) }
+
+ let_it_be(:issue_1) { create(:issue, project: project, author: project.creator, labels: [label_1, label_2]) }
+ let_it_be(:issue_2) { create(:issue, project: project, milestone: milestone, assignees: [user_1]) }
+ let_it_be(:issue_3) { create(:issue, project: project) }
+ let_it_be(:issue_outside_project) { create(:issue) }
+
+ let_it_be(:stage) do
+ create(:cycle_analytics_project_stage,
+ project: project,
+ start_event_identifier: :issue_created,
+ end_event_identifier: :issue_deployed_to_production
+ )
+ end
+
+ let_it_be(:stage_event_1) do
+ create(:cycle_analytics_issue_stage_event,
+ stage_event_hash_id: stage.stage_event_hash_id,
+ group_id: group.id,
+ project_id: project.id,
+ issue_id: issue_1.id,
+ author_id: project.creator.id,
+ milestone_id: nil,
+ state_id: issue_1.state_id,
+ end_event_timestamp: 8.months.ago
+ )
+ end
+
+ let_it_be(:stage_event_2) do
+ create(:cycle_analytics_issue_stage_event,
+ stage_event_hash_id: stage.stage_event_hash_id,
+ group_id: group.id,
+ project_id: project.id,
+ issue_id: issue_2.id,
+ author_id: nil,
+ milestone_id: milestone.id,
+ state_id: issue_2.state_id
+ )
+ end
+
+ let_it_be(:stage_event_3) do
+ create(:cycle_analytics_issue_stage_event,
+ stage_event_hash_id: stage.stage_event_hash_id,
+ group_id: group.id,
+ project_id: project.id,
+ issue_id: issue_3.id,
+ author_id: nil,
+ milestone_id: milestone.id,
+ state_id: issue_3.state_id,
+ start_event_timestamp: 8.months.ago,
+ end_event_timestamp: nil
+ )
+ end
+
+ let(:params) do
+ {
+ from: 1.year.ago.to_date,
+ to: Date.today
+ }
+ end
+
+ subject(:issue_ids) { described_class.new(stage: stage, params: params).build.pluck(:issue_id) }
+
+ it 'scopes the query for the given project' do
+ expect(issue_ids).to match_array([issue_1.id, issue_2.id])
+ expect(issue_ids).not_to include([issue_outside_project.id])
+ end
+
+ describe 'author_username param' do
+ it 'returns stage events associated with the given author' do
+ params[:author_username] = project.creator.username
+
+ expect(issue_ids).to eq([issue_1.id])
+ end
+
+ it 'returns empty result when unknown author is given' do
+ params[:author_username] = 'no one'
+
+ expect(issue_ids).to be_empty
+ end
+ end
+
+ describe 'milestone_title param' do
+ it 'returns stage events associated with the milestone' do
+ params[:milestone_title] = milestone.title
+
+ expect(issue_ids).to eq([issue_2.id])
+ end
+
+ it 'returns empty result when unknown milestone is given' do
+ params[:milestone_title] = 'unknown milestone'
+
+ expect(issue_ids).to be_empty
+ end
+ end
+
+ describe 'label_name param' do
+ it 'returns stage events associated with multiple labels' do
+ params[:label_name] = [label_1.name, label_2.name]
+
+ expect(issue_ids).to eq([issue_1.id])
+ end
+
+ it 'does not include records with partial label match' do
+ params[:label_name] = [label_1.name, 'other label']
+
+ expect(issue_ids).to be_empty
+ end
+ end
+
+ describe 'assignee_username param' do
+ it 'returns stage events associated assignee' do
+ params[:assignee_username] = [user_1.username]
+
+ expect(issue_ids).to eq([issue_2.id])
+ end
+ end
+
+ describe 'timestamp filtering' do
+ before do
+ params[:from] = 1.year.ago
+ params[:to] = 6.months.ago
+ end
+
+ it 'filters by the end event time range' do
+ expect(issue_ids).to eq([issue_1.id])
+ end
+
+ context 'when in_progress items are requested' do
+ before do
+ params[:end_event_filter] = :in_progress
+ end
+
+ it 'filters by the start event time range' do
+ expect(issue_ids).to eq([issue_3.id])
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher_spec.rb
new file mode 100644
index 00000000000..045cdb129cb
--- /dev/null
+++ b/spec/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher_spec.rb
@@ -0,0 +1,130 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Analytics::CycleAnalytics::Aggregated::RecordsFetcher do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:issue_1) { create(:issue, project: project) }
+ let_it_be(:issue_2) { create(:issue, project: project) }
+ let_it_be(:issue_3) { create(:issue, project: project) }
+
+ let_it_be(:stage_event_1) { create(:cycle_analytics_issue_stage_event, issue_id: issue_1.id, start_event_timestamp: 2.years.ago, end_event_timestamp: 1.year.ago) } # duration: 1 year
+ let_it_be(:stage_event_2) { create(:cycle_analytics_issue_stage_event, issue_id: issue_2.id, start_event_timestamp: 5.years.ago, end_event_timestamp: 2.years.ago) } # duration: 3 years
+ let_it_be(:stage_event_3) { create(:cycle_analytics_issue_stage_event, issue_id: issue_3.id, start_event_timestamp: 6.years.ago, end_event_timestamp: 3.months.ago) } # duration: 5+ years
+
+ let_it_be(:stage) { create(:cycle_analytics_project_stage, start_event_identifier: :issue_created, end_event_identifier: :issue_deployed_to_production, project: project) }
+
+ let(:params) { {} }
+
+ subject(:records_fetcher) do
+ described_class.new(stage: stage, query: Analytics::CycleAnalytics::IssueStageEvent.all, params: params)
+ end
+
+ shared_examples 'match returned records' do
+ it 'returns issues in the correct order' do
+ returned_iids = records_fetcher.serialized_records.pluck(:iid).map(&:to_i)
+
+ expect(returned_iids).to eq(expected_issue_ids)
+ end
+ end
+
+ describe '#serialized_records' do
+ describe 'sorting' do
+ context 'when sorting by end event DESC' do
+ let(:expected_issue_ids) { [issue_3.iid, issue_1.iid, issue_2.iid] }
+
+ before do
+ params[:sort] = :end_event
+ params[:direction] = :desc
+ end
+
+ it_behaves_like 'match returned records'
+ end
+
+ context 'when sorting by end event ASC' do
+ let(:expected_issue_ids) { [issue_2.iid, issue_1.iid, issue_3.iid] }
+
+ before do
+ params[:sort] = :end_event
+ params[:direction] = :asc
+ end
+
+ it_behaves_like 'match returned records'
+ end
+
+ context 'when sorting by duration DESC' do
+ let(:expected_issue_ids) { [issue_3.iid, issue_2.iid, issue_1.iid] }
+
+ before do
+ params[:sort] = :duration
+ params[:direction] = :desc
+ end
+
+ it_behaves_like 'match returned records'
+ end
+
+ context 'when sorting by duration ASC' do
+ let(:expected_issue_ids) { [issue_1.iid, issue_2.iid, issue_3.iid] }
+
+ before do
+ params[:sort] = :duration
+ params[:direction] = :asc
+ end
+
+ it_behaves_like 'match returned records'
+ end
+ end
+
+ describe 'pagination' do
+ let(:expected_issue_ids) { [issue_3.iid] }
+
+ before do
+ params[:sort] = :duration
+ params[:direction] = :asc
+ params[:page] = 2
+
+ stub_const('Gitlab::Analytics::CycleAnalytics::Aggregated::RecordsFetcher::MAX_RECORDS', 2)
+ end
+
+ it_behaves_like 'match returned records'
+ end
+
+ context 'when passing a block to serialized_records method' do
+ before do
+ params[:sort] = :duration
+ params[:direction] = :asc
+ end
+
+ it 'yields the underlying stage event scope' do
+ stage_event_records = []
+
+ records_fetcher.serialized_records do |scope|
+ stage_event_records.concat(scope.to_a)
+ end
+
+ expect(stage_event_records.map(&:issue_id)).to eq([issue_1.id, issue_2.id, issue_3.id])
+ end
+ end
+
+ context 'when the issue record no longer exists' do
+ it 'skips non-existing issue records' do
+ create(:cycle_analytics_issue_stage_event, {
+ issue_id: 0, # non-existing id
+ start_event_timestamp: 5.months.ago,
+ end_event_timestamp: 3.months.ago
+ })
+
+ stage_event_count = nil
+
+ records_fetcher.serialized_records do |scope|
+ stage_event_count = scope.to_a.size
+ end
+
+ issue_count = records_fetcher.serialized_records.to_a.size
+
+ expect(stage_event_count).to eq(4)
+ expect(issue_count).to eq(3)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/application_rate_limiter_spec.rb b/spec/lib/gitlab/application_rate_limiter_spec.rb
index 0fb99688d27..c74bcf8d678 100644
--- a/spec/lib/gitlab/application_rate_limiter_spec.rb
+++ b/spec/lib/gitlab/application_rate_limiter_spec.rb
@@ -3,76 +3,108 @@
require 'spec_helper'
RSpec.describe Gitlab::ApplicationRateLimiter do
- let(:redis) { double('redis') }
- let(:user) { create(:user) }
- let(:project) { create(:project) }
- let(:rate_limits) do
- {
- test_action: {
- threshold: 1,
- interval: 2.minutes
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+
+ subject { described_class }
+
+ describe '.throttled?', :clean_gitlab_redis_rate_limiting do
+ let(:rate_limits) do
+ {
+ test_action: {
+ threshold: 1,
+ interval: 2.minutes
+ },
+ another_action: {
+ threshold: 2,
+ interval: 3.minutes
+ }
}
- }
- end
+ end
- let(:key) { rate_limits.keys[0] }
+ before do
+ allow(described_class).to receive(:rate_limits).and_return(rate_limits)
+ end
- subject { described_class }
+ context 'when the key is invalid' do
+ context 'is provided as a Symbol' do
+ context 'but is not defined in the rate_limits Hash' do
+ it 'raises an InvalidKeyError exception' do
+ key = :key_not_in_rate_limits_hash
- before do
- allow(Gitlab::Redis::RateLimiting).to receive(:with).and_yield(redis)
- allow(described_class).to receive(:rate_limits).and_return(rate_limits)
- end
+ expect { subject.throttled?(key) }.to raise_error(Gitlab::ApplicationRateLimiter::InvalidKeyError)
+ end
+ end
+ end
- shared_examples 'action rate limiter' do
- it 'increases the throttle count and sets the expiration time' do
- expect(redis).to receive(:incr).with(cache_key).and_return(1)
- expect(redis).to receive(:expire).with(cache_key, 120)
+ context 'is provided as a String' do
+ context 'and is a String representation of an existing key in rate_limits Hash' do
+ it 'raises an InvalidKeyError exception' do
+ key = rate_limits.keys[0].to_s
- expect(subject.throttled?(key, scope: scope)).to be_falsy
- end
+ expect { subject.throttled?(key) }.to raise_error(Gitlab::ApplicationRateLimiter::InvalidKeyError)
+ end
+ end
- it 'returns true if the key is throttled' do
- expect(redis).to receive(:incr).with(cache_key).and_return(2)
- expect(redis).not_to receive(:expire)
+ context 'but is not defined in any form in the rate_limits Hash' do
+ it 'raises an InvalidKeyError exception' do
+ key = 'key_not_in_rate_limits_hash'
- expect(subject.throttled?(key, scope: scope)).to be_truthy
+ expect { subject.throttled?(key) }.to raise_error(Gitlab::ApplicationRateLimiter::InvalidKeyError)
+ end
+ end
+ end
end
- context 'when throttling is disabled' do
- it 'returns false and does not set expiration time' do
- expect(redis).not_to receive(:incr)
- expect(redis).not_to receive(:expire)
+ shared_examples 'throttles based on key and scope' do
+ let(:start_time) { Time.current.beginning_of_hour }
- expect(subject.throttled?(key, scope: scope, threshold: 0)).to be_falsy
+ it 'returns true when threshold is exceeded' do
+ travel_to(start_time) do
+ expect(subject.throttled?(:test_action, scope: scope)).to eq(false)
+ end
+
+ travel_to(start_time + 1.minute) do
+ expect(subject.throttled?(:test_action, scope: scope)).to eq(true)
+
+ # Assert that it does not affect other actions or scope
+ expect(subject.throttled?(:another_action, scope: scope)).to eq(false)
+ expect(subject.throttled?(:test_action, scope: [user])).to eq(false)
+ end
end
- end
- end
- context 'when the key is an array of only ActiveRecord models' do
- let(:scope) { [user, project] }
+ it 'returns false when interval has elapsed' do
+ travel_to(start_time) do
+ expect(subject.throttled?(:test_action, scope: scope)).to eq(false)
- let(:cache_key) do
- "application_rate_limiter:test_action:user:#{user.id}:project:#{project.id}"
- end
+ # another_action has a threshold of 3 so we simulate 2 requests
+ expect(subject.throttled?(:another_action, scope: scope)).to eq(false)
+ expect(subject.throttled?(:another_action, scope: scope)).to eq(false)
+ end
- it_behaves_like 'action rate limiter'
- end
+ travel_to(start_time + 2.minutes) do
+ expect(subject.throttled?(:test_action, scope: scope)).to eq(false)
- context 'when they key a combination of ActiveRecord models and strings' do
- let(:project) { create(:project, :public, :repository) }
- let(:commit) { project.repository.commit }
- let(:path) { 'app/controllers/groups_controller.rb' }
- let(:scope) { [project, commit, path] }
+ # Assert that another_action has its own interval that hasn't elapsed
+ expect(subject.throttled?(:another_action, scope: scope)).to eq(true)
+ end
+ end
+ end
+
+ context 'when using ActiveRecord models as scope' do
+ let(:scope) { [user, project] }
- let(:cache_key) do
- "application_rate_limiter:test_action:project:#{project.id}:commit:#{commit.sha}:#{path}"
+ it_behaves_like 'throttles based on key and scope'
end
- it_behaves_like 'action rate limiter'
+ context 'when using ActiveRecord models and strings as scope' do
+ let(:scope) { [project, 'app/controllers/groups_controller.rb'] }
+
+ it_behaves_like 'throttles based on key and scope'
+ end
end
- describe '#log_request' do
+ describe '.log_request' do
let(:file_path) { 'master/README.md' }
let(:type) { :raw_blob_request_limit }
let(:fullpath) { "/#{project.full_path}/raw/#{file_path}" }
@@ -102,7 +134,7 @@ RSpec.describe Gitlab::ApplicationRateLimiter do
end
context 'with a current_user' do
- let(:current_user) { create(:user) }
+ let(:current_user) { user }
let(:attributes) do
base_attributes.merge({
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index f3799c58fed..ac29bb22865 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -11,27 +11,13 @@ module Gitlab
allow_any_instance_of(ApplicationSetting).to receive(:current).and_return(::ApplicationSetting.create_from_defaults)
end
- context "without project" do
- let(:input) { '<b>ascii</b>' }
- let(:context) { {} }
- let(:html) { 'H<sub>2</sub>O' }
-
- it "converts the input using Asciidoctor and default options" do
- expected_asciidoc_opts = {
- safe: :secure,
- backend: :gitlab_html5,
- attributes: described_class::DEFAULT_ADOC_ATTRS.merge({ "kroki-server-url" => nil }),
- extensions: be_a(Proc)
- }
-
- expect(Asciidoctor).to receive(:convert)
- .with(input, expected_asciidoc_opts).and_return(html)
-
- expect(render(input, context)).to eq(html)
- end
+ shared_examples_for 'renders correct asciidoc' do
+ context "without project" do
+ let(:input) { '<b>ascii</b>' }
+ let(:context) { {} }
+ let(:html) { 'H<sub>2</sub>O' }
- context "with asciidoc_opts" do
- it "merges the options with default ones" do
+ it "converts the input using Asciidoctor and default options" do
expected_asciidoc_opts = {
safe: :secure,
backend: :gitlab_html5,
@@ -42,796 +28,839 @@ module Gitlab
expect(Asciidoctor).to receive(:convert)
.with(input, expected_asciidoc_opts).and_return(html)
- render(input, context)
+ expect(render(input, context)).to eq(html)
end
- end
- context "with requested path" do
- input = <<~ADOC
- Document name: {docname}.
- ADOC
-
- it "ignores {docname} when not available" do
- expect(render(input, {})).to include(input.strip)
- end
-
- [
- ['/', '', 'root'],
- ['README', 'README', 'just a filename'],
- ['doc/api/', '', 'a directory'],
- ['doc/api/README.adoc', 'README', 'a complete path']
- ].each do |path, basename, desc|
- it "sets {docname} for #{desc}" do
- expect(render(input, { requested_path: path })).to include(": #{basename}.")
- end
- end
- end
+ context "with asciidoc_opts" do
+ it "merges the options with default ones" do
+ expected_asciidoc_opts = {
+ safe: :secure,
+ backend: :gitlab_html5,
+ attributes: described_class::DEFAULT_ADOC_ATTRS.merge({ "kroki-server-url" => nil }),
+ extensions: be_a(Proc)
+ }
- context "XSS" do
- items = {
- 'link with extra attribute' => {
- input: 'link:mylink"onmouseover="alert(1)[Click Here]',
- output: "<div>\n<p><a href=\"mylink\">Click Here</a></p>\n</div>"
- },
- 'link with unsafe scheme' => {
- input: 'link:data://danger[Click Here]',
- output: "<div>\n<p><a>Click Here</a></p>\n</div>"
- },
- 'image with onerror' => {
- input: 'image:https://localhost.com/image.png[Alt text" onerror="alert(7)]',
- output: "<div>\n<p><span><a class=\"no-attachment-icon\" href=\"https://localhost.com/image.png\" target=\"_blank\" rel=\"noopener noreferrer\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt='Alt text\" onerror=\"alert(7)' class=\"lazy\" data-src=\"https://localhost.com/image.png\"></a></span></p>\n</div>"
- },
- 'fenced code with inline script' => {
- input: '```mypre"><script>alert(3)</script>',
- output: "<div>\n<div>\n<pre class=\"code highlight js-syntax-highlight language-plaintext\" lang=\"plaintext\" v-pre=\"true\"><code><span id=\"LC1\" class=\"line\" lang=\"plaintext\">\"&gt;</span></code></pre>\n</div>\n</div>"
- }
- }
+ expect(Asciidoctor).to receive(:convert)
+ .with(input, expected_asciidoc_opts).and_return(html)
- items.each do |name, data|
- it "does not convert dangerous #{name} into HTML" do
- expect(render(data[:input], context)).to include(data[:output])
+ render(input, context)
end
end
- it 'does not allow locked attributes to be overridden' do
+ context "with requested path" do
input = <<~ADOC
- {counter:max-include-depth:1234}
- <|-- {max-include-depth}
+ Document name: {docname}.
ADOC
- expect(render(input, {})).not_to include('1234')
- end
- end
+ it "ignores {docname} when not available" do
+ expect(render(input, {})).to include(input.strip)
+ end
- context "images" do
- it "does lazy load and link image" do
- input = 'image:https://localhost.com/image.png[]'
- output = "<div>\n<p><span><a class=\"no-attachment-icon\" href=\"https://localhost.com/image.png\" target=\"_blank\" rel=\"noopener noreferrer\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"image\" class=\"lazy\" data-src=\"https://localhost.com/image.png\"></a></span></p>\n</div>"
- expect(render(input, context)).to include(output)
+ [
+ ['/', '', 'root'],
+ ['README', 'README', 'just a filename'],
+ ['doc/api/', '', 'a directory'],
+ ['doc/api/README.adoc', 'README', 'a complete path']
+ ].each do |path, basename, desc|
+ it "sets {docname} for #{desc}" do
+ expect(render(input, { requested_path: path })).to include(": #{basename}.")
+ end
+ end
end
- it "does not automatically link image if link is explicitly defined" do
- input = 'image:https://localhost.com/image.png[link=https://gitlab.com]'
- output = "<div>\n<p><span><a href=\"https://gitlab.com\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"image\" class=\"lazy\" data-src=\"https://localhost.com/image.png\"></a></span></p>\n</div>"
- expect(render(input, context)).to include(output)
- end
- end
+ context "XSS" do
+ items = {
+ 'link with extra attribute' => {
+ input: 'link:mylink"onmouseover="alert(1)[Click Here]',
+ output: "<div>\n<p><a href=\"mylink\">Click Here</a></p>\n</div>"
+ },
+ 'link with unsafe scheme' => {
+ input: 'link:data://danger[Click Here]',
+ output: "<div>\n<p><a>Click Here</a></p>\n</div>"
+ },
+ 'image with onerror' => {
+ input: 'image:https://localhost.com/image.png[Alt text" onerror="alert(7)]',
+ output: "<div>\n<p><span><a class=\"no-attachment-icon\" href=\"https://localhost.com/image.png\" target=\"_blank\" rel=\"noopener noreferrer\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt='Alt text\" onerror=\"alert(7)' class=\"lazy\" data-src=\"https://localhost.com/image.png\"></a></span></p>\n</div>"
+ }
+ }
- context 'with admonition' do
- it 'preserves classes' do
- input = <<~ADOC
- NOTE: An admonition paragraph, like this note, grabs the reader’s attention.
- ADOC
+ items.each do |name, data|
+ it "does not convert dangerous #{name} into HTML" do
+ expect(render(data[:input], context)).to include(data[:output])
+ end
+ end
- output = <<~HTML
- <div class="admonitionblock">
- <table>
- <tr>
- <td class="icon">
- <i class="fa icon-note" title="Note"></i>
- </td>
- <td>
- An admonition paragraph, like this note, grabs the reader’s attention.
- </td>
- </tr>
- </table>
- </div>
- HTML
-
- expect(render(input, context)).to include(output.strip)
- end
- end
+ # `stub_feature_flags method` runs AFTER declaration of `items` above.
+ # So the spec in its current implementation won't pass.
+ # Move this test back to the items hash when removing `use_cmark_renderer` feature flag.
+ it "does not convert dangerous fenced code with inline script into HTML" do
+ input = '```mypre"><script>alert(3)</script>'
+ output =
+ if Feature.enabled?(:use_cmark_renderer)
+ "<div>\n<div>\n<pre class=\"code highlight js-syntax-highlight language-plaintext\" lang=\"plaintext\" v-pre=\"true\"><code></code></pre>\n</div>\n</div>"
+ else
+ "<div>\n<div>\n<pre class=\"code highlight js-syntax-highlight language-plaintext\" lang=\"plaintext\" v-pre=\"true\"><code><span id=\"LC1\" class=\"line\" lang=\"plaintext\">\"&gt;</span></code></pre>\n</div>\n</div>"
+ end
- context 'with passthrough' do
- it 'removes non heading ids' do
- input = <<~ADOC
- ++++
- <h2 id="foo">Title</h2>
- ++++
- ADOC
+ expect(render(input, context)).to include(output)
+ end
- output = <<~HTML
- <h2>Title</h2>
- HTML
+ it 'does not allow locked attributes to be overridden' do
+ input = <<~ADOC
+ {counter:max-include-depth:1234}
+ <|-- {max-include-depth}
+ ADOC
- expect(render(input, context)).to include(output.strip)
+ expect(render(input, {})).not_to include('1234')
+ end
end
- it 'removes non footnote def ids' do
- input = <<~ADOC
- ++++
- <div id="def">Footnote definition</div>
- ++++
- ADOC
-
- output = <<~HTML
- <div>Footnote definition</div>
- HTML
+ context "images" do
+ it "does lazy load and link image" do
+ input = 'image:https://localhost.com/image.png[]'
+ output = "<div>\n<p><span><a class=\"no-attachment-icon\" href=\"https://localhost.com/image.png\" target=\"_blank\" rel=\"noopener noreferrer\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"image\" class=\"lazy\" data-src=\"https://localhost.com/image.png\"></a></span></p>\n</div>"
+ expect(render(input, context)).to include(output)
+ end
- expect(render(input, context)).to include(output.strip)
+ it "does not automatically link image if link is explicitly defined" do
+ input = 'image:https://localhost.com/image.png[link=https://gitlab.com]'
+ output = "<div>\n<p><span><a href=\"https://gitlab.com\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"image\" class=\"lazy\" data-src=\"https://localhost.com/image.png\"></a></span></p>\n</div>"
+ expect(render(input, context)).to include(output)
+ end
end
- it 'removes non footnote ref ids' do
- input = <<~ADOC
- ++++
- <a id="ref">Footnote reference</a>
- ++++
- ADOC
-
- output = <<~HTML
- <a>Footnote reference</a>
- HTML
+ context 'with admonition' do
+ it 'preserves classes' do
+ input = <<~ADOC
+ NOTE: An admonition paragraph, like this note, grabs the reader’s attention.
+ ADOC
- expect(render(input, context)).to include(output.strip)
+ output = <<~HTML
+ <div class="admonitionblock">
+ <table>
+ <tr>
+ <td class="icon">
+ <i class="fa icon-note" title="Note"></i>
+ </td>
+ <td>
+ An admonition paragraph, like this note, grabs the reader’s attention.
+ </td>
+ </tr>
+ </table>
+ </div>
+ HTML
+
+ expect(render(input, context)).to include(output.strip)
+ end
end
- end
- context 'with footnotes' do
- it 'preserves ids and links' do
- input = <<~ADOC
- This paragraph has a footnote.footnote:[This is the text of the footnote.]
- ADOC
-
- output = <<~HTML
- <div>
- <p>This paragraph has a footnote.<sup>[<a id="_footnoteref_1" href="#_footnotedef_1" title="View footnote.">1</a>]</sup></p>
- </div>
- <div>
- <hr>
- <div id="_footnotedef_1">
- <a href="#_footnoteref_1">1</a>. This is the text of the footnote.
- </div>
- </div>
- HTML
-
- expect(render(input, context)).to include(output.strip)
- end
- end
+ context 'with passthrough' do
+ it 'removes non heading ids' do
+ input = <<~ADOC
+ ++++
+ <h2 id="foo">Title</h2>
+ ++++
+ ADOC
- context 'with section anchors' do
- it 'preserves ids and links' do
- input = <<~ADOC
- = Title
+ output = <<~HTML
+ <h2>Title</h2>
+ HTML
- == First section
+ expect(render(input, context)).to include(output.strip)
+ end
- This is the first section.
+ it 'removes non footnote def ids' do
+ input = <<~ADOC
+ ++++
+ <div id="def">Footnote definition</div>
+ ++++
+ ADOC
- == Second section
+ output = <<~HTML
+ <div>Footnote definition</div>
+ HTML
- This is the second section.
+ expect(render(input, context)).to include(output.strip)
+ end
- == Thunder ⚡ !
+ it 'removes non footnote ref ids' do
+ input = <<~ADOC
+ ++++
+ <a id="ref">Footnote reference</a>
+ ++++
+ ADOC
- This is the third section.
- ADOC
+ output = <<~HTML
+ <a>Footnote reference</a>
+ HTML
- output = <<~HTML
- <h1>Title</h1>
- <div>
- <h2 id="user-content-first-section">
- <a class="anchor" href="#user-content-first-section"></a>First section</h2>
- <div>
- <div>
- <p>This is the first section.</p>
- </div>
- </div>
- </div>
- <div>
- <h2 id="user-content-second-section">
- <a class="anchor" href="#user-content-second-section"></a>Second section</h2>
- <div>
- <div>
- <p>This is the second section.</p>
- </div>
- </div>
- </div>
- <div>
- <h2 id="user-content-thunder">
- <a class="anchor" href="#user-content-thunder"></a>Thunder ⚡ !</h2>
- <div>
- <div>
- <p>This is the third section.</p>
- </div>
- </div>
- </div>
- HTML
-
- expect(render(input, context)).to include(output.strip)
+ expect(render(input, context)).to include(output.strip)
+ end
end
- end
-
- context 'with xrefs' do
- it 'preserves ids' do
- input = <<~ADOC
- Learn how to xref:cross-references[use cross references].
- [[cross-references]]A link to another location within an AsciiDoc document or between AsciiDoc documents is called a cross reference (also referred to as an xref).
- ADOC
-
- output = <<~HTML
- <div>
- <p>Learn how to <a href="#cross-references">use cross references</a>.</p>
- </div>
- <div>
- <p><a id="user-content-cross-references"></a>A link to another location within an AsciiDoc document or between AsciiDoc documents is called a cross reference (also referred to as an xref).</p>
- </div>
- HTML
+ context 'with footnotes' do
+ it 'preserves ids and links' do
+ input = <<~ADOC
+ This paragraph has a footnote.footnote:[This is the text of the footnote.]
+ ADOC
- expect(render(input, context)).to include(output.strip)
+ output = <<~HTML
+ <div>
+ <p>This paragraph has a footnote.<sup>[<a id="_footnoteref_1" href="#_footnotedef_1" title="View footnote.">1</a>]</sup></p>
+ </div>
+ <div>
+ <hr>
+ <div id="_footnotedef_1">
+ <a href="#_footnoteref_1">1</a>. This is the text of the footnote.
+ </div>
+ </div>
+ HTML
+
+ expect(render(input, context)).to include(output.strip)
+ end
end
- end
- context 'with checklist' do
- it 'preserves classes' do
- input = <<~ADOC
- * [x] checked
- * [ ] not checked
- ADOC
+ context 'with section anchors' do
+ it 'preserves ids and links' do
+ input = <<~ADOC
+ = Title
+
+ == First section
+
+ This is the first section.
+
+ == Second section
+
+ This is the second section.
+
+ == Thunder ⚡ !
+
+ This is the third section.
+ ADOC
- output = <<~HTML
- <div>
- <ul class="checklist">
- <li>
- <p><i class="fa fa-check-square-o"></i> checked</p>
- </li>
- <li>
- <p><i class="fa fa-square-o"></i> not checked</p>
- </li>
- </ul>
- </div>
- HTML
-
- expect(render(input, context)).to include(output.strip)
+ output = <<~HTML
+ <h1>Title</h1>
+ <div>
+ <h2 id="user-content-first-section">
+ <a class="anchor" href="#user-content-first-section"></a>First section</h2>
+ <div>
+ <div>
+ <p>This is the first section.</p>
+ </div>
+ </div>
+ </div>
+ <div>
+ <h2 id="user-content-second-section">
+ <a class="anchor" href="#user-content-second-section"></a>Second section</h2>
+ <div>
+ <div>
+ <p>This is the second section.</p>
+ </div>
+ </div>
+ </div>
+ <div>
+ <h2 id="user-content-thunder">
+ <a class="anchor" href="#user-content-thunder"></a>Thunder ⚡ !</h2>
+ <div>
+ <div>
+ <p>This is the third section.</p>
+ </div>
+ </div>
+ </div>
+ HTML
+
+ expect(render(input, context)).to include(output.strip)
+ end
end
- end
-
- context 'with marks' do
- it 'preserves classes' do
- input = <<~ADOC
- Werewolves are allergic to #cassia cinnamon#.
-
- Did the werewolves read the [.small]#small print#?
- Where did all the [.underline.small]#cores# run off to?
+ context 'with xrefs' do
+ it 'preserves ids' do
+ input = <<~ADOC
+ Learn how to xref:cross-references[use cross references].
+
+ [[cross-references]]A link to another location within an AsciiDoc document or between AsciiDoc documents is called a cross reference (also referred to as an xref).
+ ADOC
- We need [.line-through]#ten# make that twenty VMs.
+ output = <<~HTML
+ <div>
+ <p>Learn how to <a href="#cross-references">use cross references</a>.</p>
+ </div>
+ <div>
+ <p><a id="user-content-cross-references"></a>A link to another location within an AsciiDoc document or between AsciiDoc documents is called a cross reference (also referred to as an xref).</p>
+ </div>
+ HTML
- [.big]##O##nce upon an infinite loop.
- ADOC
-
- output = <<~HTML
- <div>
- <p>Werewolves are allergic to <mark>cassia cinnamon</mark>.</p>
- </div>
- <div>
- <p>Did the werewolves read the <span class="small">small print</span>?</p>
- </div>
- <div>
- <p>Where did all the <span class="underline small">cores</span> run off to?</p>
- </div>
- <div>
- <p>We need <span class="line-through">ten</span> make that twenty VMs.</p>
- </div>
- <div>
- <p><span class="big">O</span>nce upon an infinite loop.</p>
- </div>
- HTML
-
- expect(render(input, context)).to include(output.strip)
+ expect(render(input, context)).to include(output.strip)
+ end
end
- end
- context 'with fenced block' do
- it 'highlights syntax' do
- input = <<~ADOC
- ```js
- console.log('hello world')
- ```
- ADOC
-
- output = <<~HTML
- <div>
- <div>
- <pre class="code highlight js-syntax-highlight language-javascript" lang="javascript" v-pre="true"><code><span id="LC1" class="line" lang="javascript"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">hello world</span><span class="dl">'</span><span class="p">)</span></span></code></pre>
- </div>
- </div>
- HTML
+ context 'with checklist' do
+ it 'preserves classes' do
+ input = <<~ADOC
+ * [x] checked
+ * [ ] not checked
+ ADOC
- expect(render(input, context)).to include(output.strip)
+ output = <<~HTML
+ <div>
+ <ul class="checklist">
+ <li>
+ <p><i class="fa fa-check-square-o"></i> checked</p>
+ </li>
+ <li>
+ <p><i class="fa fa-square-o"></i> not checked</p>
+ </li>
+ </ul>
+ </div>
+ HTML
+
+ expect(render(input, context)).to include(output.strip)
+ end
end
- end
- context 'with listing block' do
- it 'highlights syntax' do
- input = <<~ADOC
- [source,c++]
- .class.cpp
- ----
- #include <stdio.h>
-
- for (int i = 0; i < 5; i++) {
- std::cout<<"*"<<std::endl;
- }
- ----
- ADOC
+ context 'with marks' do
+ it 'preserves classes' do
+ input = <<~ADOC
+ Werewolves are allergic to #cassia cinnamon#.
+
+ Did the werewolves read the [.small]#small print#?
+
+ Where did all the [.underline.small]#cores# run off to?
+
+ We need [.line-through]#ten# make that twenty VMs.
+
+ [.big]##O##nce upon an infinite loop.
+ ADOC
- output = <<~HTML
- <div>
- <div>class.cpp</div>
- <div>
- <pre class="code highlight js-syntax-highlight language-cpp" lang="cpp" v-pre="true"><code><span id="LC1" class="line" lang="cpp"><span class="cp">#include &lt;stdio.h&gt;</span></span>
- <span id="LC2" class="line" lang="cpp"></span>
- <span id="LC3" class="line" lang="cpp"><span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">5</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span></span>
- <span id="LC4" class="line" lang="cpp"> <span class="n">std</span><span class="o">::</span><span class="n">cout</span><span class="o">&lt;&lt;</span><span class="s">"*"</span><span class="o">&lt;&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span></span>
- <span id="LC5" class="line" lang="cpp"><span class="p">}</span></span></code></pre>
- </div>
- </div>
- HTML
-
- expect(render(input, context)).to include(output.strip)
+ output = <<~HTML
+ <div>
+ <p>Werewolves are allergic to <mark>cassia cinnamon</mark>.</p>
+ </div>
+ <div>
+ <p>Did the werewolves read the <span class="small">small print</span>?</p>
+ </div>
+ <div>
+ <p>Where did all the <span class="underline small">cores</span> run off to?</p>
+ </div>
+ <div>
+ <p>We need <span class="line-through">ten</span> make that twenty VMs.</p>
+ </div>
+ <div>
+ <p><span class="big">O</span>nce upon an infinite loop.</p>
+ </div>
+ HTML
+
+ expect(render(input, context)).to include(output.strip)
+ end
end
- end
- context 'with stem block' do
- it 'does not apply syntax highlighting' do
- input = <<~ADOC
- [stem]
- ++++
- \sqrt{4} = 2
- ++++
- ADOC
+ context 'with fenced block' do
+ it 'highlights syntax' do
+ input = <<~ADOC
+ ```js
+ console.log('hello world')
+ ```
+ ADOC
- output = "<div>\n<div>\n\\$ qrt{4} = 2\\$\n</div>\n</div>"
+ output = <<~HTML
+ <div>
+ <div>
+ <pre class="code highlight js-syntax-highlight language-javascript" lang="javascript" v-pre="true"><code><span id="LC1" class="line" lang="javascript"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">hello world</span><span class="dl">'</span><span class="p">)</span></span></code></pre>
+ </div>
+ </div>
+ HTML
- expect(render(input, context)).to include(output)
+ expect(render(input, context)).to include(output.strip)
+ end
end
- end
- context 'external links' do
- it 'adds the `rel` attribute to the link' do
- output = render('link:https://google.com[Google]', context)
+ context 'with listing block' do
+ it 'highlights syntax' do
+ input = <<~ADOC
+ [source,c++]
+ .class.cpp
+ ----
+ #include <stdio.h>
+
+ for (int i = 0; i < 5; i++) {
+ std::cout<<"*"<<std::endl;
+ }
+ ----
+ ADOC
- expect(output).to include('rel="nofollow noreferrer noopener"')
+ output = <<~HTML
+ <div>
+ <div>class.cpp</div>
+ <div>
+ <pre class="code highlight js-syntax-highlight language-cpp" lang="cpp" v-pre="true"><code><span id="LC1" class="line" lang="cpp"><span class="cp">#include &lt;stdio.h&gt;</span></span>
+ <span id="LC2" class="line" lang="cpp"></span>
+ <span id="LC3" class="line" lang="cpp"><span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">5</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span></span>
+ <span id="LC4" class="line" lang="cpp"> <span class="n">std</span><span class="o">::</span><span class="n">cout</span><span class="o">&lt;&lt;</span><span class="s">"*"</span><span class="o">&lt;&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span></span>
+ <span id="LC5" class="line" lang="cpp"><span class="p">}</span></span></code></pre>
+ </div>
+ </div>
+ HTML
+
+ expect(render(input, context)).to include(output.strip)
+ end
end
- end
- context 'LaTex code' do
- it 'adds class js-render-math to the output' do
- input = <<~MD
- :stem: latexmath
-
- [stem]
- ++++
- \sqrt{4} = 2
- ++++
-
- another part
-
- [latexmath]
- ++++
- \beta_x \gamma
- ++++
+ context 'with stem block' do
+ it 'does not apply syntax highlighting' do
+ input = <<~ADOC
+ [stem]
+ ++++
+ \sqrt{4} = 2
+ ++++
+ ADOC
- stem:[2+2] is 4
- MD
+ output = "<div>\n<div>\n\\$ qrt{4} = 2\\$\n</div>\n</div>"
- expect(render(input, context)).to include('<pre data-math-style="display" class="code math js-render-math"><code>eta_x gamma</code></pre>')
- expect(render(input, context)).to include('<p><code data-math-style="inline" class="code math js-render-math">2+2</code> is 4</p>')
+ expect(render(input, context)).to include(output)
+ end
end
- end
- context 'outfilesuffix' do
- it 'defaults to adoc' do
- output = render("Inter-document reference <<README.adoc#>>", context)
+ context 'external links' do
+ it 'adds the `rel` attribute to the link' do
+ output = render('link:https://google.com[Google]', context)
- expect(output).to include("a href=\"README.adoc\"")
+ expect(output).to include('rel="nofollow noreferrer noopener"')
+ end
end
- end
- context 'with mermaid diagrams' do
- it 'adds class js-render-mermaid to the output' do
- input = <<~MD
- [mermaid]
- ....
- graph LR
- A[Square Rect] -- Link text --> B((Circle))
- A --> C(Round Rect)
- B --> D{Rhombus}
- C --> D
- ....
- MD
-
- output = <<~HTML
- <pre data-mermaid-style="display" class="js-render-mermaid">graph LR
- A[Square Rect] -- Link text --&gt; B((Circle))
- A --&gt; C(Round Rect)
- B --&gt; D{Rhombus}
- C --&gt; D</pre>
- HTML
-
- expect(render(input, context)).to include(output.strip)
+ context 'LaTex code' do
+ it 'adds class js-render-math to the output' do
+ input = <<~MD
+ :stem: latexmath
+
+ [stem]
+ ++++
+ \sqrt{4} = 2
+ ++++
+
+ another part
+
+ [latexmath]
+ ++++
+ \beta_x \gamma
+ ++++
+
+ stem:[2+2] is 4
+ MD
+
+ expect(render(input, context)).to include('<pre data-math-style="display" class="code math js-render-math"><code>eta_x gamma</code></pre>')
+ expect(render(input, context)).to include('<p><code data-math-style="inline" class="code math js-render-math">2+2</code> is 4</p>')
+ end
end
- it 'applies subs in diagram block' do
- input = <<~MD
- :class-name: AveryLongClass
+ context 'outfilesuffix' do
+ it 'defaults to adoc' do
+ output = render("Inter-document reference <<README.adoc#>>", context)
- [mermaid,subs=+attributes]
- ....
- classDiagram
- Class01 <|-- {class-name} : Cool
- ....
- MD
+ expect(output).to include("a href=\"README.adoc\"")
+ end
+ end
- output = <<~HTML
- <pre data-mermaid-style="display" class="js-render-mermaid">classDiagram
- Class01 &lt;|-- AveryLongClass : Cool</pre>
- HTML
+ context 'with mermaid diagrams' do
+ it 'adds class js-render-mermaid to the output' do
+ input = <<~MD
+ [mermaid]
+ ....
+ graph LR
+ A[Square Rect] -- Link text --> B((Circle))
+ A --> C(Round Rect)
+ B --> D{Rhombus}
+ C --> D
+ ....
+ MD
+
+ output = <<~HTML
+ <pre data-mermaid-style="display" class="js-render-mermaid">graph LR
+ A[Square Rect] -- Link text --&gt; B((Circle))
+ A --&gt; C(Round Rect)
+ B --&gt; D{Rhombus}
+ C --&gt; D</pre>
+ HTML
+
+ expect(render(input, context)).to include(output.strip)
+ end
- expect(render(input, context)).to include(output.strip)
+ it 'applies subs in diagram block' do
+ input = <<~MD
+ :class-name: AveryLongClass
+
+ [mermaid,subs=+attributes]
+ ....
+ classDiagram
+ Class01 <|-- {class-name} : Cool
+ ....
+ MD
+
+ output = <<~HTML
+ <pre data-mermaid-style="display" class="js-render-mermaid">classDiagram
+ Class01 &lt;|-- AveryLongClass : Cool</pre>
+ HTML
+
+ expect(render(input, context)).to include(output.strip)
+ end
end
- end
- context 'with Kroki 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')
- end
+ context 'with Kroki 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')
+ end
- it 'converts a graphviz diagram to image' do
- input = <<~ADOC
- [graphviz]
- ....
- digraph G {
- Hello->World
- }
- ....
- ADOC
+ it 'converts a graphviz diagram to image' do
+ input = <<~ADOC
+ [graphviz]
+ ....
+ digraph G {
+ Hello->World
+ }
+ ....
+ ADOC
- output = <<~HTML
- <div>
- <div>
- <a class="no-attachment-icon" href="https://kroki.io/graphviz/svg/eNpLyUwvSizIUHBXqOZSUPBIzcnJ17ULzy_KSeGqBQCEzQka" target="_blank" rel="noopener noreferrer"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Diagram" class="lazy" data-src="https://kroki.io/graphviz/svg/eNpLyUwvSizIUHBXqOZSUPBIzcnJ17ULzy_KSeGqBQCEzQka"></a>
- </div>
- </div>
- HTML
+ output = <<~HTML
+ <div>
+ <div>
+ <a class="no-attachment-icon" href="https://kroki.io/graphviz/svg/eNpLyUwvSizIUHBXqOZSUPBIzcnJ17ULzy_KSeGqBQCEzQka" target="_blank" rel="noopener noreferrer"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Diagram" class="lazy" data-src="https://kroki.io/graphviz/svg/eNpLyUwvSizIUHBXqOZSUPBIzcnJ17ULzy_KSeGqBQCEzQka"></a>
+ </div>
+ </div>
+ HTML
- expect(render(input, context)).to include(output.strip)
- end
+ 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
+ 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
+ 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
- it 'does not allow kroki-plantuml-include to be overridden' do
- input = <<~ADOC
- [plantuml, test="{counter:kroki-plantuml-include:/etc/passwd}", format="png"]
- ....
- class BlockProcessor
+ it 'does not allow kroki-plantuml-include to be overridden' do
+ input = <<~ADOC
+ [plantuml, test="{counter:kroki-plantuml-include:/etc/passwd}", format="png"]
+ ....
+ class BlockProcessor
+
+ BlockProcessor <|-- {counter:kroki-plantuml-include}
+ ....
+ ADOC
- BlockProcessor <|-- {counter:kroki-plantuml-include}
- ....
- ADOC
+ output = <<~HTML
+ <div>
+ <div>
+ <a class=\"no-attachment-icon\" href=\"https://kroki.io/plantuml/png/eNpLzkksLlZwyslPzg4oyk9OLS7OL-LiQuUr2NTo6ipUJ-eX5pWkFlllF-VnZ-oW5CTmlZTm5uhm5iXnlKak1gIABQEb8A==\" target=\"_blank\" rel=\"noopener noreferrer\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"Diagram\" class=\"lazy\" data-src=\"https://kroki.io/plantuml/png/eNpLzkksLlZwyslPzg4oyk9OLS7OL-LiQuUr2NTo6ipUJ-eX5pWkFlllF-VnZ-oW5CTmlZTm5uhm5iXnlKak1gIABQEb8A==\"></a>
+ </div>
+ </div>
+ HTML
+
+ expect(render(input, {})).to include(output.strip)
+ end
- output = <<~HTML
- <div>
- <div>
- <a class=\"no-attachment-icon\" href=\"https://kroki.io/plantuml/png/eNpLzkksLlZwyslPzg4oyk9OLS7OL-LiQuUr2NTo6ipUJ-eX5pWkFlllF-VnZ-oW5CTmlZTm5uhm5iXnlKak1gIABQEb8A==\" target=\"_blank\" rel=\"noopener noreferrer\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"Diagram\" class=\"lazy\" data-src=\"https://kroki.io/plantuml/png/eNpLzkksLlZwyslPzg4oyk9OLS7OL-LiQuUr2NTo6ipUJ-eX5pWkFlllF-VnZ-oW5CTmlZTm5uhm5iXnlKak1gIABQEb8A==\"></a>
- </div>
- </div>
- HTML
+ it 'does not allow kroki-server-url to be overridden' do
+ input = <<~ADOC
+ [plantuml, test="{counter:kroki-server-url:evilsite}", format="png"]
+ ....
+ class BlockProcessor
+
+ BlockProcessor
+ ....
+ ADOC
- expect(render(input, {})).to include(output.strip)
+ expect(render(input, {})).not_to include('evilsite')
+ end
end
- it 'does not allow kroki-server-url to be overridden' do
- input = <<~ADOC
- [plantuml, test="{counter:kroki-server-url:evilsite}", format="png"]
- ....
- class BlockProcessor
+ 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
- BlockProcessor
- ....
- ADOC
+ 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
- expect(render(input, {})).not_to include('evilsite')
+ 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
- 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)
+ context 'with project' do
+ let(:context) do
+ {
+ commit: commit,
+ project: project,
+ ref: ref,
+ requested_path: requested_path
+ }
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
+ let(:commit) { project.commit(ref) }
+ let(:project) { create(:project, :repository) }
+ let(:ref) { 'asciidoc' }
+ let(:requested_path) { '/' }
- expect(render(input, context)).to include(output.strip)
- end
- end
- end
+ context 'include directive' do
+ subject(:output) { render(input, context) }
- context 'with project' do
- let(:context) do
- {
- commit: commit,
- project: project,
- ref: ref,
- requested_path: requested_path
- }
- end
+ let(:input) { "Include this:\n\ninclude::#{include_path}[]" }
- let(:commit) { project.commit(ref) }
- let(:project) { create(:project, :repository) }
- let(:ref) { 'asciidoc' }
- let(:requested_path) { '/' }
-
- context 'include directive' do
- subject(:output) { render(input, context) }
+ before do
+ current_file = requested_path
+ current_file += 'README.adoc' if requested_path.end_with? '/'
- let(:input) { "Include this:\n\ninclude::#{include_path}[]" }
+ create_file(current_file, "= AsciiDoc\n")
+ end
- before do
- current_file = requested_path
- current_file += 'README.adoc' if requested_path.end_with? '/'
+ def many_includes(target)
+ Array.new(10, "include::#{target}[]").join("\n")
+ end
- create_file(current_file, "= AsciiDoc\n")
- end
+ context 'cyclic imports' do
+ before do
+ create_file('doc/api/a.adoc', many_includes('b.adoc'))
+ create_file('doc/api/b.adoc', many_includes('a.adoc'))
+ end
- def many_includes(target)
- Array.new(10, "include::#{target}[]").join("\n")
- end
+ let(:include_path) { 'a.adoc' }
+ let(:requested_path) { 'doc/api/README.md' }
- context 'cyclic imports' do
- before do
- create_file('doc/api/a.adoc', many_includes('b.adoc'))
- create_file('doc/api/b.adoc', many_includes('a.adoc'))
+ it 'completes successfully' do
+ is_expected.to include('<p>Include this:</p>')
+ end
end
- let(:include_path) { 'a.adoc' }
- let(:requested_path) { 'doc/api/README.md' }
+ context 'with path to non-existing file' do
+ let(:include_path) { 'not-exists.adoc' }
- it 'completes successfully' do
- is_expected.to include('<p>Include this:</p>')
+ it 'renders Unresolved directive placeholder' do
+ is_expected.to include("<strong>[ERROR: include::#{include_path}[] - unresolved directive]</strong>")
+ end
end
- end
- context 'with path to non-existing file' do
- let(:include_path) { 'not-exists.adoc' }
+ shared_examples :invalid_include do
+ let(:include_path) { 'dk.png' }
- it 'renders Unresolved directive placeholder' do
- is_expected.to include("<strong>[ERROR: include::#{include_path}[] - unresolved directive]</strong>")
- end
- end
+ before do
+ allow(project.repository).to receive(:blob_at).and_return(blob)
+ end
- shared_examples :invalid_include do
- let(:include_path) { 'dk.png' }
+ it 'does not read the blob' do
+ expect(blob).not_to receive(:data)
+ end
- before do
- allow(project.repository).to receive(:blob_at).and_return(blob)
+ it 'renders Unresolved directive placeholder' do
+ is_expected.to include("<strong>[ERROR: include::#{include_path}[] - unresolved directive]</strong>")
+ end
end
- it 'does not read the blob' do
- expect(blob).not_to receive(:data)
- end
+ context 'with path to a binary file' do
+ let(:blob) { fake_blob(path: 'dk.png', binary: true) }
- it 'renders Unresolved directive placeholder' do
- is_expected.to include("<strong>[ERROR: include::#{include_path}[] - unresolved directive]</strong>")
+ include_examples :invalid_include
end
- end
-
- context 'with path to a binary file' do
- let(:blob) { fake_blob(path: 'dk.png', binary: true) }
- include_examples :invalid_include
- end
+ context 'with path to file in external storage' do
+ let(:blob) { fake_blob(path: 'dk.png', lfs: true) }
- context 'with path to file in external storage' do
- let(:blob) { fake_blob(path: 'dk.png', lfs: true) }
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ project.update_attribute(:lfs_enabled, true)
+ end
- before do
- allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
- project.update_attribute(:lfs_enabled, true)
+ include_examples :invalid_include
end
- include_examples :invalid_include
- end
+ context 'with path to a textual file' do
+ let(:include_path) { 'sample.adoc' }
- context 'with path to a textual file' do
- let(:include_path) { 'sample.adoc' }
+ before do
+ create_file(file_path, "Content from #{include_path}")
+ end
- before do
- create_file(file_path, "Content from #{include_path}")
- end
-
- shared_examples :valid_include do
- [
- ['/doc/sample.adoc', 'doc/sample.adoc', 'absolute path'],
- ['sample.adoc', 'doc/api/sample.adoc', 'relative path'],
- ['./sample.adoc', 'doc/api/sample.adoc', 'relative path with leading ./'],
- ['../sample.adoc', 'doc/sample.adoc', 'relative path to a file up one directory'],
- ['../../sample.adoc', 'sample.adoc', 'relative path for a file up multiple directories']
- ].each do |include_path_, file_path_, desc|
- context "the file is specified by #{desc}" do
- let(:include_path) { include_path_ }
- let(:file_path) { file_path_ }
-
- it 'includes content of the file' do
- is_expected.to include('<p>Include this:</p>')
- is_expected.to include("<p>Content from #{include_path}</p>")
+ shared_examples :valid_include do
+ [
+ ['/doc/sample.adoc', 'doc/sample.adoc', 'absolute path'],
+ ['sample.adoc', 'doc/api/sample.adoc', 'relative path'],
+ ['./sample.adoc', 'doc/api/sample.adoc', 'relative path with leading ./'],
+ ['../sample.adoc', 'doc/sample.adoc', 'relative path to a file up one directory'],
+ ['../../sample.adoc', 'sample.adoc', 'relative path for a file up multiple directories']
+ ].each do |include_path_, file_path_, desc|
+ context "the file is specified by #{desc}" do
+ let(:include_path) { include_path_ }
+ let(:file_path) { file_path_ }
+
+ it 'includes content of the file' do
+ is_expected.to include('<p>Include this:</p>')
+ is_expected.to include("<p>Content from #{include_path}</p>")
+ end
end
end
end
- end
- context 'when requested path is a file in the repo' do
- let(:requested_path) { 'doc/api/README.adoc' }
+ context 'when requested path is a file in the repo' do
+ let(:requested_path) { 'doc/api/README.adoc' }
- include_examples :valid_include
+ include_examples :valid_include
- context 'without a commit (only ref)' do
- let(:commit) { nil }
+ context 'without a commit (only ref)' do
+ let(:commit) { nil }
- include_examples :valid_include
+ include_examples :valid_include
+ end
end
- end
- context 'when requested path is a directory in the repo' do
- let(:requested_path) { 'doc/api/' }
+ context 'when requested path is a directory in the repo' do
+ let(:requested_path) { 'doc/api/' }
- include_examples :valid_include
+ include_examples :valid_include
- context 'without a commit (only ref)' do
- let(:commit) { nil }
+ context 'without a commit (only ref)' do
+ let(:commit) { nil }
- include_examples :valid_include
+ include_examples :valid_include
+ end
end
end
- end
-
- context 'when repository is passed into the context' do
- let(:wiki_repo) { project.wiki.repository }
- let(:include_path) { 'wiki_file.adoc' }
- before do
- project.create_wiki
- context.merge!(repository: wiki_repo)
- end
+ context 'when repository is passed into the context' do
+ let(:wiki_repo) { project.wiki.repository }
+ let(:include_path) { 'wiki_file.adoc' }
- context 'when the file exists' do
before do
- create_file(include_path, 'Content from wiki', repository: wiki_repo)
+ project.create_wiki
+ context.merge!(repository: wiki_repo)
end
- it { is_expected.to include('<p>Content from wiki</p>') }
- end
-
- context 'when the file does not exist' do
- it { is_expected.to include("[ERROR: include::#{include_path}[] - unresolved directive]")}
- end
- end
-
- context 'recursive includes with relative paths' do
- let(:input) do
- <<~ADOC
- Source: requested file
+ context 'when the file exists' do
+ before do
+ create_file(include_path, 'Content from wiki', repository: wiki_repo)
+ end
- include::doc/README.adoc[]
+ it { is_expected.to include('<p>Content from wiki</p>') }
+ end
- include::license.adoc[]
- ADOC
+ context 'when the file does not exist' do
+ it { is_expected.to include("[ERROR: include::#{include_path}[] - unresolved directive]")}
+ end
end
- before do
- create_file 'doc/README.adoc', <<~ADOC
- Source: doc/README.adoc
-
- include::../license.adoc[]
+ context 'recursive includes with relative paths' do
+ let(:input) do
+ <<~ADOC
+ Source: requested file
+
+ include::doc/README.adoc[]
+
+ include::license.adoc[]
+ ADOC
+ end
- include::api/hello.adoc[]
- ADOC
- create_file 'license.adoc', <<~ADOC
- Source: license.adoc
- ADOC
- create_file 'doc/api/hello.adoc', <<~ADOC
- Source: doc/api/hello.adoc
+ before do
+ create_file 'doc/README.adoc', <<~ADOC
+ Source: doc/README.adoc
+
+ include::../license.adoc[]
+
+ include::api/hello.adoc[]
+ ADOC
+ create_file 'license.adoc', <<~ADOC
+ Source: license.adoc
+ ADOC
+ create_file 'doc/api/hello.adoc', <<~ADOC
+ Source: doc/api/hello.adoc
+
+ include::./common.adoc[]
+ ADOC
+ create_file 'doc/api/common.adoc', <<~ADOC
+ Source: doc/api/common.adoc
+ ADOC
+ end
- include::./common.adoc[]
- ADOC
- create_file 'doc/api/common.adoc', <<~ADOC
- Source: doc/api/common.adoc
- ADOC
+ it 'includes content of the included files recursively' do
+ expect(output.gsub(/<[^>]+>/, '').gsub(/\n\s*/, "\n").strip).to eq <<~ADOC.strip
+ Source: requested file
+ Source: doc/README.adoc
+ Source: license.adoc
+ Source: doc/api/hello.adoc
+ Source: doc/api/common.adoc
+ Source: license.adoc
+ ADOC
+ end
end
- it 'includes content of the included files recursively' do
- expect(output.gsub(/<[^>]+>/, '').gsub(/\n\s*/, "\n").strip).to eq <<~ADOC.strip
- Source: requested file
- Source: doc/README.adoc
- Source: license.adoc
- Source: doc/api/hello.adoc
- Source: doc/api/common.adoc
- Source: license.adoc
- ADOC
+ def create_file(path, content, repository: project.repository)
+ repository.create_file(project.creator, path, content,
+ message: "Add #{path}", branch_name: 'asciidoc')
end
end
+ end
+ end
- def create_file(path, content, repository: project.repository)
- repository.create_file(project.creator, path, content,
- message: "Add #{path}", branch_name: 'asciidoc')
- end
+ context 'using ruby-based HTML renderer' do
+ before do
+ stub_feature_flags(use_cmark_renderer: false)
+ end
+
+ it_behaves_like 'renders correct asciidoc'
+ end
+
+ context 'using c-based HTML renderer' do
+ before do
+ stub_feature_flags(use_cmark_renderer: true)
end
+
+ it_behaves_like 'renders correct asciidoc'
end
def render(*args)
diff --git a/spec/lib/gitlab/auth/auth_finders_spec.rb b/spec/lib/gitlab/auth/auth_finders_spec.rb
index b0522e269e0..f1c891b2adb 100644
--- a/spec/lib/gitlab/auth/auth_finders_spec.rb
+++ b/spec/lib/gitlab/auth/auth_finders_spec.rb
@@ -873,45 +873,65 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
describe '#find_user_from_job_token' do
+ let(:token) { job.token }
+
subject { find_user_from_job_token }
- context 'when the token is in the headers' do
- before do
- set_header(described_class::JOB_TOKEN_HEADER, token)
+ shared_examples 'finds user when job token allowed' do
+ context 'when the token is in the headers' do
+ before do
+ set_header(described_class::JOB_TOKEN_HEADER, token)
+ end
+
+ it_behaves_like 'find user from job token'
end
- it_behaves_like 'find user from job token'
- end
+ context 'when the token is in the job_token param' do
+ before do
+ set_param(described_class::JOB_TOKEN_PARAM, token)
+ end
- context 'when the token is in the job_token param' do
- before do
- set_param(described_class::JOB_TOKEN_PARAM, token)
+ it_behaves_like 'find user from job token'
end
- it_behaves_like 'find user from job token'
- end
+ context 'when the token is in the token param' do
+ before do
+ set_param(described_class::RUNNER_JOB_TOKEN_PARAM, token)
+ end
- context 'when the token is in the token param' do
- before do
- set_param(described_class::RUNNER_JOB_TOKEN_PARAM, token)
+ it_behaves_like 'find user from job token'
end
+ end
- it_behaves_like 'find user from job token'
+ context 'when route setting allows job_token' do
+ let(:route_authentication_setting) { { job_token_allowed: true } }
+
+ include_examples 'finds user when job token allowed'
end
- context 'when the job token is provided via basic auth' do
+ context 'when route setting is basic auth' do
let(:route_authentication_setting) { { job_token_allowed: :basic_auth } }
- let(:username) { ::Gitlab::Auth::CI_JOB_USER }
- let(:token) { job.token }
- before do
- set_basic_auth_header(username, token)
+ context 'when the token is provided via basic auth' do
+ let(:username) { ::Gitlab::Auth::CI_JOB_USER }
+
+ before do
+ set_basic_auth_header(username, token)
+ end
+
+ it { is_expected.to eq(user) }
end
- it { is_expected.to eq(user) }
+ include_examples 'finds user when job token allowed'
+ end
- context 'credentials are provided but route setting is incorrect' do
- let(:route_authentication_setting) { { job_token_allowed: :unknown } }
+ context 'when route setting job_token_allowed is invalid' do
+ let(:route_authentication_setting) { { job_token_allowed: false } }
+
+ context 'when the token is provided' do
+ before do
+ set_header(described_class::JOB_TOKEN_HEADER, token)
+ end
it { is_expected.to be_nil }
end
diff --git a/spec/lib/gitlab/background_migration/add_modified_to_approval_merge_request_rule_spec.rb b/spec/lib/gitlab/background_migration/add_modified_to_approval_merge_request_rule_spec.rb
index 81b8b5dde08..0b29163671c 100644
--- a/spec/lib/gitlab/background_migration/add_modified_to_approval_merge_request_rule_spec.rb
+++ b/spec/lib/gitlab/background_migration/add_modified_to_approval_merge_request_rule_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::AddModifiedToApprovalMergeRequestRule, schema: 20200817195628 do
+RSpec.describe Gitlab::BackgroundMigration::AddModifiedToApprovalMergeRequestRule, schema: 20181228175414 do
let(:determine_if_rules_are_modified) { described_class.new }
let(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab') }
diff --git a/spec/lib/gitlab/background_migration/add_primary_email_to_emails_if_user_confirmed_spec.rb b/spec/lib/gitlab/background_migration/add_primary_email_to_emails_if_user_confirmed_spec.rb
new file mode 100644
index 00000000000..b50a55a9e41
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/add_primary_email_to_emails_if_user_confirmed_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::AddPrimaryEmailToEmailsIfUserConfirmed do
+ let(:users) { table(:users) }
+ let(:emails) { table(:emails) }
+
+ let!(:unconfirmed_user) { users.create!(name: 'unconfirmed', email: 'unconfirmed@example.com', confirmed_at: nil, projects_limit: 100) }
+ let!(:confirmed_user_1) { users.create!(name: 'confirmed-1', email: 'confirmed-1@example.com', confirmed_at: 1.day.ago, projects_limit: 100) }
+ let!(:confirmed_user_2) { users.create!(name: 'confirmed-2', email: 'confirmed-2@example.com', confirmed_at: 1.day.ago, projects_limit: 100) }
+ let!(:email) { emails.create!(user_id: confirmed_user_1.id, email: 'confirmed-1@example.com', confirmed_at: 1.day.ago) }
+
+ let(:perform) { described_class.new.perform(users.first.id, users.last.id) }
+
+ it 'adds the primary email of confirmed users to Emails, unless already added', :aggregate_failures do
+ expect(emails.where(email: [unconfirmed_user.email, confirmed_user_2.email])).to be_empty
+
+ expect { perform }.not_to raise_error
+
+ expect(emails.where(email: unconfirmed_user.email).count).to eq(0)
+ expect(emails.where(email: confirmed_user_1.email, user_id: confirmed_user_1.id).count).to eq(1)
+ expect(emails.where(email: confirmed_user_2.email, user_id: confirmed_user_2.id).count).to eq(1)
+
+ email_2 = emails.find_by(email: confirmed_user_2.email, user_id: confirmed_user_2.id)
+ expect(email_2.confirmed_at).to eq(confirmed_user_2.reload.confirmed_at)
+ end
+
+ it 'sets timestamps on the created Emails' do
+ perform
+
+ email_2 = emails.find_by(email: confirmed_user_2.email, user_id: confirmed_user_2.id)
+
+ expect(email_2.created_at).not_to be_nil
+ expect(email_2.updated_at).not_to be_nil
+ end
+
+ context 'when a range of IDs is specified' do
+ let!(:confirmed_user_3) { users.create!(name: 'confirmed-3', email: 'confirmed-3@example.com', confirmed_at: 1.hour.ago, projects_limit: 100) }
+ let!(:confirmed_user_4) { users.create!(name: 'confirmed-4', email: 'confirmed-4@example.com', confirmed_at: 1.hour.ago, projects_limit: 100) }
+
+ it 'only acts on the specified range of IDs', :aggregate_failures do
+ expect do
+ described_class.new.perform(confirmed_user_2.id, confirmed_user_3.id)
+ end.to change { Email.count }.by(2)
+ expect(emails.where(email: confirmed_user_4.email).count).to eq(0)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/backfill_artifact_expiry_date_spec.rb b/spec/lib/gitlab/background_migration/backfill_artifact_expiry_date_spec.rb
index 49fa7b41916..6ab1e3ecd70 100644
--- a/spec/lib/gitlab/background_migration/backfill_artifact_expiry_date_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_artifact_expiry_date_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::BackfillArtifactExpiryDate, :migration, schema: 20201111152859 do
+RSpec.describe Gitlab::BackgroundMigration::BackfillArtifactExpiryDate, :migration, schema: 20181228175414 do
subject(:perform) { migration.perform(1, 99) }
let(:migration) { described_class.new }
diff --git a/spec/lib/gitlab/background_migration/backfill_deployment_clusters_from_deployments_spec.rb b/spec/lib/gitlab/background_migration/backfill_deployment_clusters_from_deployments_spec.rb
index 54c14e7a4b8..1404ada3647 100644
--- a/spec/lib/gitlab/background_migration/backfill_deployment_clusters_from_deployments_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_deployment_clusters_from_deployments_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::BackfillDeploymentClustersFromDeployments, :migration, schema: 20200227140242 do
+RSpec.describe Gitlab::BackgroundMigration::BackfillDeploymentClustersFromDeployments, :migration, schema: 20181228175414 do
subject { described_class.new }
describe '#perform' do
diff --git a/spec/lib/gitlab/background_migration/backfill_design_internal_ids_spec.rb b/spec/lib/gitlab/background_migration/backfill_design_internal_ids_spec.rb
deleted file mode 100644
index 4bf59a02a31..00000000000
--- a/spec/lib/gitlab/background_migration/backfill_design_internal_ids_spec.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::BackgroundMigration::BackfillDesignInternalIds, :migration, schema: 20201030203854 do
- subject { described_class.new(designs) }
-
- let_it_be(:namespaces) { table(:namespaces) }
- let_it_be(:projects) { table(:projects) }
- let_it_be(:designs) { table(:design_management_designs) }
-
- let(:namespace) { namespaces.create!(name: 'foo', path: 'foo') }
- let(:project) { projects.create!(namespace_id: namespace.id) }
- let(:project_2) { projects.create!(namespace_id: namespace.id) }
-
- def create_design!(proj = project)
- designs.create!(project_id: proj.id, filename: generate(:filename))
- end
-
- def migrate!
- relation = designs.where(project_id: [project.id, project_2.id]).select(:project_id).distinct
-
- subject.perform(relation)
- end
-
- it 'backfills the iid for designs' do
- 3.times { create_design! }
-
- expect do
- migrate!
- end.to change { designs.pluck(:iid) }.from(contain_exactly(nil, nil, nil)).to(contain_exactly(1, 2, 3))
- end
-
- it 'scopes IIDs and handles range and starting-point correctly' do
- create_design!.update!(iid: 10)
- create_design!.update!(iid: 12)
- create_design!(project_2).update!(iid: 7)
- project_3 = projects.create!(namespace_id: namespace.id)
-
- 2.times { create_design! }
- 2.times { create_design!(project_2) }
- 2.times { create_design!(project_3) }
-
- migrate!
-
- expect(designs.where(project_id: project.id).pluck(:iid)).to contain_exactly(10, 12, 13, 14)
- expect(designs.where(project_id: project_2.id).pluck(:iid)).to contain_exactly(7, 8, 9)
- expect(designs.where(project_id: project_3.id).pluck(:iid)).to contain_exactly(nil, nil)
- end
-
- it 'updates the internal ID records' do
- design = create_design!
- 2.times { create_design! }
- design.update!(iid: 10)
- scope = { project_id: project.id }
- usage = :design_management_designs
- init = ->(_d, _s) { 0 }
-
- ::InternalId.track_greatest(design, scope, usage, 10, init)
-
- migrate!
-
- next_iid = ::InternalId.generate_next(design, scope, usage, init)
-
- expect(designs.pluck(:iid)).to contain_exactly(10, 11, 12)
- expect(design.reload.iid).to eq(10)
- expect(next_iid).to eq(13)
- end
-end
diff --git a/spec/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests_spec.rb b/spec/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests_spec.rb
index 550bdc484c9..9194525e713 100644
--- a/spec/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::BackfillEnvironmentIdDeploymentMergeRequests, schema: 20200312134637 do
+RSpec.describe Gitlab::BackgroundMigration::BackfillEnvironmentIdDeploymentMergeRequests, schema: 20181228175414 do
let(:environments) { table(:environments) }
let(:merge_requests) { table(:merge_requests) }
let(:deployments) { table(:deployments) }
diff --git a/spec/lib/gitlab/background_migration/backfill_jira_tracker_deployment_type2_spec.rb b/spec/lib/gitlab/background_migration/backfill_jira_tracker_deployment_type2_spec.rb
index 58864aac084..446d62bbd2a 100644
--- a/spec/lib/gitlab/background_migration/backfill_jira_tracker_deployment_type2_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_jira_tracker_deployment_type2_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::BackfillJiraTrackerDeploymentType2, :migration, schema: 20201028182809 do
+RSpec.describe Gitlab::BackgroundMigration::BackfillJiraTrackerDeploymentType2, :migration, schema: 20181228175414 do
let_it_be(:jira_integration_temp) { described_class::JiraServiceTemp }
let_it_be(:jira_tracker_data_temp) { described_class::JiraTrackerDataTemp }
let_it_be(:atlassian_host) { 'https://api.atlassian.net' }
diff --git a/spec/lib/gitlab/background_migration/backfill_merge_request_cleanup_schedules_spec.rb b/spec/lib/gitlab/background_migration/backfill_merge_request_cleanup_schedules_spec.rb
index c2daa35703d..d33f52514da 100644
--- a/spec/lib/gitlab/background_migration/backfill_merge_request_cleanup_schedules_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_merge_request_cleanup_schedules_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::BackfillMergeRequestCleanupSchedules, schema: 20201103110018 do
+RSpec.describe Gitlab::BackgroundMigration::BackfillMergeRequestCleanupSchedules, schema: 20181228175414 do
let(:merge_requests) { table(:merge_requests) }
let(:cleanup_schedules) { table(:merge_request_cleanup_schedules) }
let(:metrics) { table(:merge_request_metrics) }
diff --git a/spec/lib/gitlab/background_migration/backfill_namespace_settings_spec.rb b/spec/lib/gitlab/background_migration/backfill_namespace_settings_spec.rb
index 43e76a2952e..0f8adca2ca4 100644
--- a/spec/lib/gitlab/background_migration/backfill_namespace_settings_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_namespace_settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::BackfillNamespaceSettings, schema: 20200703125016 do
+RSpec.describe Gitlab::BackgroundMigration::BackfillNamespaceSettings, schema: 20181228175414 do
let(:namespaces) { table(:namespaces) }
let(:namespace_settings) { table(:namespace_settings) }
let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
diff --git a/spec/lib/gitlab/background_migration/backfill_project_settings_spec.rb b/spec/lib/gitlab/background_migration/backfill_project_settings_spec.rb
index 48c5674822a..e6b0db2ab73 100644
--- a/spec/lib/gitlab/background_migration/backfill_project_settings_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_project_settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::BackfillProjectSettings, schema: 20200114113341 do
+RSpec.describe Gitlab::BackgroundMigration::BackfillProjectSettings, schema: 20181228175414 do
let(:projects) { table(:projects) }
let(:project_settings) { table(:project_settings) }
let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
diff --git a/spec/lib/gitlab/background_migration/backfill_push_rules_id_in_projects_spec.rb b/spec/lib/gitlab/background_migration/backfill_push_rules_id_in_projects_spec.rb
index 9ce6a3227b5..3468df3dccd 100644
--- a/spec/lib/gitlab/background_migration/backfill_push_rules_id_in_projects_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_push_rules_id_in_projects_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::BackfillPushRulesIdInProjects, :migration, schema: 2020_03_25_162730 do
+RSpec.describe Gitlab::BackgroundMigration::BackfillPushRulesIdInProjects, :migration, schema: 20181228175414 do
let(:push_rules) { table(:push_rules) }
let(:projects) { table(:projects) }
let(:project_settings) { table(:project_settings) }
diff --git a/spec/lib/gitlab/background_migration/backfill_user_namespace_spec.rb b/spec/lib/gitlab/background_migration/backfill_user_namespace_spec.rb
new file mode 100644
index 00000000000..395248b786d
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_user_namespace_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillUserNamespace, :migration, schema: 20210930211936 do
+ let(:migration) { described_class.new }
+ let(:namespaces_table) { table(:namespaces) }
+
+ let(:table_name) { 'namespaces' }
+ let(:batch_column) { :id }
+ let(:sub_batch_size) { 100 }
+ let(:pause_ms) { 0 }
+
+ subject(:perform_migration) { migration.perform(1, 10, table_name, batch_column, sub_batch_size, pause_ms) }
+
+ before do
+ namespaces_table.create!(id: 1, name: 'test1', path: 'test1', type: nil)
+ namespaces_table.create!(id: 2, name: 'test2', path: 'test2', type: 'User')
+ namespaces_table.create!(id: 3, name: 'test3', path: 'test3', type: 'Group')
+ namespaces_table.create!(id: 4, name: 'test4', path: 'test4', type: nil)
+ namespaces_table.create!(id: 11, name: 'test11', path: 'test11', type: nil)
+ end
+
+ it 'backfills `type` for the selected records', :aggregate_failures do
+ queries = ActiveRecord::QueryRecorder.new do
+ perform_migration
+ end
+
+ expect(queries.count).to eq(3)
+ expect(namespaces_table.where(type: 'User').count).to eq 3
+ expect(namespaces_table.where(type: 'User').pluck(:id)).to match_array([1, 2, 4])
+ end
+
+ it 'tracks timings of queries' do
+ expect(migration.batch_metrics.timings).to be_empty
+
+ expect { perform_migration }.to change { migration.batch_metrics.timings }
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb b/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb
index 3e378db04d4..d4fc24d0559 100644
--- a/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb
+++ b/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJob do
- let(:table_name) { :copy_primary_key_test }
+ let(:table_name) { :_test_copy_primary_key_test }
let(:test_table) { table(table_name) }
let(:sub_batch_size) { 1000 }
let(:pause_ms) { 0 }
diff --git a/spec/lib/gitlab/background_migration/copy_merge_request_target_project_to_merge_request_metrics_spec.rb b/spec/lib/gitlab/background_migration/copy_merge_request_target_project_to_merge_request_metrics_spec.rb
deleted file mode 100644
index 71bb794d539..00000000000
--- a/spec/lib/gitlab/background_migration/copy_merge_request_target_project_to_merge_request_metrics_spec.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::BackgroundMigration::CopyMergeRequestTargetProjectToMergeRequestMetrics, :migration, schema: 20200723125205 do
- let(:migration) { described_class.new }
-
- let_it_be(:namespaces) { table(:namespaces) }
- let_it_be(:projects) { table(:projects) }
- let_it_be(:merge_requests) { table(:merge_requests) }
- let_it_be(:metrics) { table(:merge_request_metrics) }
-
- let!(:namespace) { namespaces.create!(name: 'namespace', path: 'namespace') }
- let!(:project_1) { projects.create!(namespace_id: namespace.id) }
- let!(:project_2) { projects.create!(namespace_id: namespace.id) }
- let!(:merge_request_to_migrate_1) { merge_requests.create!(source_branch: 'a', target_branch: 'b', target_project_id: project_1.id) }
- let!(:merge_request_to_migrate_2) { merge_requests.create!(source_branch: 'c', target_branch: 'd', target_project_id: project_2.id) }
- let!(:merge_request_without_metrics) { merge_requests.create!(source_branch: 'e', target_branch: 'f', target_project_id: project_2.id) }
-
- let!(:metrics_1) { metrics.create!(merge_request_id: merge_request_to_migrate_1.id) }
- let!(:metrics_2) { metrics.create!(merge_request_id: merge_request_to_migrate_2.id) }
-
- let(:merge_request_ids) { [merge_request_to_migrate_1.id, merge_request_to_migrate_2.id, merge_request_without_metrics.id] }
-
- subject { migration.perform(merge_request_ids.min, merge_request_ids.max) }
-
- it 'copies `target_project_id` to the associated `merge_request_metrics` record' do
- subject
-
- expect(metrics_1.reload.target_project_id).to eq(project_1.id)
- expect(metrics_2.reload.target_project_id).to eq(project_2.id)
- end
-
- it 'does not create metrics record when it is missing' do
- subject
-
- expect(metrics.find_by_merge_request_id(merge_request_without_metrics.id)).to be_nil
- end
-end
diff --git a/spec/lib/gitlab/background_migration/drop_invalid_vulnerabilities_spec.rb b/spec/lib/gitlab/background_migration/drop_invalid_vulnerabilities_spec.rb
index c4beb719e1e..b83dc6fff7a 100644
--- a/spec/lib/gitlab/background_migration/drop_invalid_vulnerabilities_spec.rb
+++ b/spec/lib/gitlab/background_migration/drop_invalid_vulnerabilities_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::DropInvalidVulnerabilities, schema: 20201110110454 do
+RSpec.describe Gitlab::BackgroundMigration::DropInvalidVulnerabilities, schema: 20181228175414 do
let_it_be(:background_migration_jobs) { table(:background_migration_jobs) }
let_it_be(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
let_it_be(:users) { table(:users) }
diff --git a/spec/lib/gitlab/background_migration/fix_merge_request_diff_commit_users_spec.rb b/spec/lib/gitlab/background_migration/fix_merge_request_diff_commit_users_spec.rb
new file mode 100644
index 00000000000..c343ee438b8
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/fix_merge_request_diff_commit_users_spec.rb
@@ -0,0 +1,316 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# The underlying migration relies on the global models (e.g. Project). This
+# means we also need to use FactoryBot factories to ensure everything is
+# operating using the same types. If we use `table()` and similar methods we
+# would have to duplicate a lot of logic just for these tests.
+#
+# rubocop: disable RSpec/FactoriesInMigrationSpecs
+RSpec.describe Gitlab::BackgroundMigration::FixMergeRequestDiffCommitUsers do
+ let(:migration) { described_class.new }
+
+ describe '#perform' do
+ context 'when the project exists' do
+ it 'processes the project' do
+ project = create(:project)
+
+ expect(migration).to receive(:process).with(project)
+ expect(migration).to receive(:schedule_next_job)
+
+ migration.perform(project.id)
+ end
+
+ it 'marks the background job as finished' do
+ project = create(:project)
+
+ Gitlab::Database::BackgroundMigrationJob.create!(
+ class_name: 'FixMergeRequestDiffCommitUsers',
+ arguments: [project.id]
+ )
+
+ migration.perform(project.id)
+
+ job = Gitlab::Database::BackgroundMigrationJob
+ .find_by(class_name: 'FixMergeRequestDiffCommitUsers')
+
+ expect(job.status).to eq('succeeded')
+ end
+ end
+
+ context 'when the project does not exist' do
+ it 'does nothing' do
+ expect(migration).not_to receive(:process)
+ expect(migration).to receive(:schedule_next_job)
+
+ migration.perform(-1)
+ end
+ end
+ end
+
+ describe '#process' do
+ it 'processes the merge requests of the project' do
+ project = create(:project, :repository)
+ commit = project.commit
+ mr = create(
+ :merge_request_with_diffs,
+ source_project: project,
+ target_project: project
+ )
+
+ diff = mr.merge_request_diffs.first
+
+ create(
+ :merge_request_diff_commit,
+ merge_request_diff: diff,
+ sha: commit.sha,
+ relative_order: 9000
+ )
+
+ migration.process(project)
+
+ updated = diff
+ .merge_request_diff_commits
+ .find_by(sha: commit.sha, relative_order: 9000)
+
+ expect(updated.commit_author_id).not_to be_nil
+ expect(updated.committer_id).not_to be_nil
+ end
+ end
+
+ describe '#update_commit' do
+ let(:project) { create(:project, :repository) }
+ let(:mr) do
+ create(
+ :merge_request_with_diffs,
+ source_project: project,
+ target_project: project
+ )
+ end
+
+ let(:diff) { mr.merge_request_diffs.first }
+ let(:commit) { project.commit }
+
+ def update_row(migration, project, diff, row)
+ migration.update_commit(project, row)
+
+ diff
+ .merge_request_diff_commits
+ .find_by(sha: row.sha, relative_order: row.relative_order)
+ end
+
+ it 'populates missing commit authors' do
+ commit_row = create(
+ :merge_request_diff_commit,
+ merge_request_diff: diff,
+ sha: commit.sha,
+ relative_order: 9000
+ )
+
+ updated = update_row(migration, project, diff, commit_row)
+
+ expect(updated.commit_author.name).to eq(commit.to_hash[:author_name])
+ expect(updated.commit_author.email).to eq(commit.to_hash[:author_email])
+ end
+
+ it 'populates missing committers' do
+ commit_row = create(
+ :merge_request_diff_commit,
+ merge_request_diff: diff,
+ sha: commit.sha,
+ relative_order: 9000
+ )
+
+ updated = update_row(migration, project, diff, commit_row)
+
+ expect(updated.committer.name).to eq(commit.to_hash[:committer_name])
+ expect(updated.committer.email).to eq(commit.to_hash[:committer_email])
+ end
+
+ it 'leaves existing commit authors as-is' do
+ user = create(:merge_request_diff_commit_user)
+ commit_row = create(
+ :merge_request_diff_commit,
+ merge_request_diff: diff,
+ sha: commit.sha,
+ relative_order: 9000,
+ commit_author: user
+ )
+
+ updated = update_row(migration, project, diff, commit_row)
+
+ expect(updated.commit_author).to eq(user)
+ end
+
+ it 'leaves existing committers as-is' do
+ user = create(:merge_request_diff_commit_user)
+ commit_row = create(
+ :merge_request_diff_commit,
+ merge_request_diff: diff,
+ sha: commit.sha,
+ relative_order: 9000,
+ committer: user
+ )
+
+ updated = update_row(migration, project, diff, commit_row)
+
+ expect(updated.committer).to eq(user)
+ end
+
+ it 'does nothing when both the author and committer are present' do
+ user = create(:merge_request_diff_commit_user)
+ commit_row = create(
+ :merge_request_diff_commit,
+ merge_request_diff: diff,
+ sha: commit.sha,
+ relative_order: 9000,
+ committer: user,
+ commit_author: user
+ )
+
+ recorder = ActiveRecord::QueryRecorder.new do
+ migration.update_commit(project, commit_row)
+ end
+
+ expect(recorder.count).to be_zero
+ end
+
+ it 'does nothing if the commit does not exist in Git' do
+ user = create(:merge_request_diff_commit_user)
+ commit_row = create(
+ :merge_request_diff_commit,
+ merge_request_diff: diff,
+ sha: 'kittens',
+ relative_order: 9000,
+ committer: user,
+ commit_author: user
+ )
+
+ recorder = ActiveRecord::QueryRecorder.new do
+ migration.update_commit(project, commit_row)
+ end
+
+ expect(recorder.count).to be_zero
+ end
+
+ it 'does nothing when the committer/author are missing in the Git commit' do
+ user = create(:merge_request_diff_commit_user)
+ commit_row = create(
+ :merge_request_diff_commit,
+ merge_request_diff: diff,
+ sha: commit.sha,
+ relative_order: 9000,
+ committer: user,
+ commit_author: user
+ )
+
+ allow(migration).to receive(:find_or_create_user).and_return(nil)
+
+ recorder = ActiveRecord::QueryRecorder.new do
+ migration.update_commit(project, commit_row)
+ end
+
+ expect(recorder.count).to be_zero
+ end
+ end
+
+ describe '#schedule_next_job' do
+ it 'schedules the next background migration' do
+ Gitlab::Database::BackgroundMigrationJob
+ .create!(class_name: 'FixMergeRequestDiffCommitUsers', arguments: [42])
+
+ expect(BackgroundMigrationWorker)
+ .to receive(:perform_in)
+ .with(2.minutes, 'FixMergeRequestDiffCommitUsers', [42])
+
+ migration.schedule_next_job
+ end
+
+ it 'does nothing when there are no jobs' do
+ expect(BackgroundMigrationWorker)
+ .not_to receive(:perform_in)
+
+ migration.schedule_next_job
+ end
+ end
+
+ describe '#find_commit' do
+ let(:project) { create(:project, :repository) }
+
+ it 'finds a commit using Git' do
+ commit = project.commit
+ found = migration.find_commit(project, commit.sha)
+
+ expect(found).to eq(commit.to_hash)
+ end
+
+ it 'caches the results' do
+ commit = project.commit
+
+ migration.find_commit(project, commit.sha)
+
+ expect { migration.find_commit(project, commit.sha) }
+ .not_to change { Gitlab::GitalyClient.get_request_count }
+ end
+
+ it 'returns an empty hash if the commit does not exist' do
+ expect(migration.find_commit(project, 'kittens')).to eq({})
+ end
+ end
+
+ describe '#find_or_create_user' do
+ let(:project) { create(:project, :repository) }
+
+ it 'creates missing users' do
+ commit = project.commit.to_hash
+ id = migration.find_or_create_user(commit, :author_name, :author_email)
+
+ expect(MergeRequest::DiffCommitUser.count).to eq(1)
+
+ created = MergeRequest::DiffCommitUser.first
+
+ expect(created.name).to eq(commit[:author_name])
+ expect(created.email).to eq(commit[:author_email])
+ expect(created.id).to eq(id)
+ end
+
+ it 'returns users that already exist' do
+ commit = project.commit.to_hash
+ user1 = migration.find_or_create_user(commit, :author_name, :author_email)
+ user2 = migration.find_or_create_user(commit, :author_name, :author_email)
+
+ expect(user1).to eq(user2)
+ end
+
+ it 'caches the results' do
+ commit = project.commit.to_hash
+
+ migration.find_or_create_user(commit, :author_name, :author_email)
+
+ recorder = ActiveRecord::QueryRecorder.new do
+ migration.find_or_create_user(commit, :author_name, :author_email)
+ end
+
+ expect(recorder.count).to be_zero
+ end
+
+ it 'returns nil if the commit details are missing' do
+ id = migration.find_or_create_user({}, :author_name, :author_email)
+
+ expect(id).to be_nil
+ end
+ end
+
+ describe '#matches_row' do
+ it 'returns the query matches for the composite primary key' do
+ row = double(:commit, merge_request_diff_id: 4, relative_order: 5)
+ arel = migration.matches_row(row)
+
+ expect(arel.to_sql).to eq(
+ '("merge_request_diff_commits"."merge_request_diff_id", "merge_request_diff_commits"."relative_order") = (4, 5)'
+ )
+ end
+ end
+end
+# rubocop: enable RSpec/FactoriesInMigrationSpecs
diff --git a/spec/lib/gitlab/background_migration/fix_projects_without_project_feature_spec.rb b/spec/lib/gitlab/background_migration/fix_projects_without_project_feature_spec.rb
deleted file mode 100644
index d503824041b..00000000000
--- a/spec/lib/gitlab/background_migration/fix_projects_without_project_feature_spec.rb
+++ /dev/null
@@ -1,75 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::BackgroundMigration::FixProjectsWithoutProjectFeature, schema: 2020_01_27_111840 do
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:project_features) { table(:project_features) }
-
- let(:namespace) { namespaces.create!(name: 'foo', path: 'foo') }
-
- let!(:project) { projects.create!(namespace_id: namespace.id) }
- let(:private_project_without_feature) { projects.create!(namespace_id: namespace.id, visibility_level: 0) }
- let(:public_project_without_feature) { projects.create!(namespace_id: namespace.id, visibility_level: 20) }
- let!(:projects_without_feature) { [private_project_without_feature, public_project_without_feature] }
-
- before do
- project_features.create!({ project_id: project.id, pages_access_level: 20 })
- end
-
- subject { described_class.new.perform(Project.minimum(:id), Project.maximum(:id)) }
-
- def project_feature_records
- project_features.order(:project_id).pluck(:project_id)
- end
-
- def features(project)
- project_features.find_by(project_id: project.id)&.attributes
- end
-
- it 'creates a ProjectFeature for projects without it' do
- expect { subject }.to change { project_feature_records }.from([project.id]).to([project.id, *projects_without_feature.map(&:id)])
- end
-
- it 'creates ProjectFeature records with default values for a public project' do
- subject
-
- expect(features(public_project_without_feature)).to include(
- {
- "merge_requests_access_level" => 20,
- "issues_access_level" => 20,
- "wiki_access_level" => 20,
- "snippets_access_level" => 20,
- "builds_access_level" => 20,
- "repository_access_level" => 20,
- "pages_access_level" => 20,
- "forking_access_level" => 20
- }
- )
- end
-
- it 'creates ProjectFeature records with default values for a private project' do
- subject
-
- expect(features(private_project_without_feature)).to include("pages_access_level" => 10)
- end
-
- context 'when access control to pages is forced' do
- before do
- allow(::Gitlab::Pages).to receive(:access_control_is_forced?).and_return(true)
- end
-
- it 'creates ProjectFeature records with default values for a public project' do
- subject
-
- expect(features(public_project_without_feature)).to include("pages_access_level" => 10)
- end
- end
-
- it 'sets created_at/updated_at timestamps' do
- subject
-
- expect(project_features.where('created_at IS NULL OR updated_at IS NULL')).to be_empty
- end
-end
diff --git a/spec/lib/gitlab/background_migration/fix_projects_without_prometheus_service_spec.rb b/spec/lib/gitlab/background_migration/fix_projects_without_prometheus_service_spec.rb
deleted file mode 100644
index 9a497a9e01a..00000000000
--- a/spec/lib/gitlab/background_migration/fix_projects_without_prometheus_service_spec.rb
+++ /dev/null
@@ -1,234 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::BackgroundMigration::FixProjectsWithoutPrometheusService, :migration, schema: 2020_02_20_115023 do
- def service_params_for(project_id, params = {})
- {
- project_id: project_id,
- active: false,
- properties: '{}',
- type: 'PrometheusService',
- template: false,
- push_events: true,
- issues_events: true,
- merge_requests_events: true,
- tag_push_events: true,
- note_events: true,
- category: 'monitoring',
- default: false,
- wiki_page_events: true,
- pipeline_events: true,
- confidential_issues_events: true,
- commit_events: true,
- job_events: true,
- confidential_note_events: true,
- deployment_events: false
- }.merge(params)
- end
-
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:services) { table(:services) }
- let(:clusters) { table(:clusters) }
- let(:cluster_groups) { table(:cluster_groups) }
- let(:clusters_applications_prometheus) { table(:clusters_applications_prometheus) }
- let(:namespace) { namespaces.create!(name: 'user', path: 'user') }
- let(:project) { projects.create!(namespace_id: namespace.id) }
-
- let(:application_statuses) do
- {
- errored: -1,
- installed: 3,
- updated: 5
- }
- end
-
- let(:cluster_types) do
- {
- instance_type: 1,
- group_type: 2,
- project_type: 3
- }
- end
-
- let(:columns) do
- %w(project_id active properties type template push_events
- issues_events merge_requests_events tag_push_events
- note_events category default wiki_page_events pipeline_events
- confidential_issues_events commit_events job_events
- confidential_note_events deployment_events)
- end
-
- describe '#perform' do
- shared_examples 'fix services entries state' do
- it 'is idempotent' do
- expect { subject.perform(project.id, project.id + 1) }.to change { services.order(:id).map { |row| row.attributes } }
-
- expect { subject.perform(project.id, project.id + 1) }.not_to change { services.order(:id).map { |row| row.attributes } }
- end
-
- context 'non prometheus services' do
- it 'does not change them' do
- other_type = 'SomeOtherService'
- services.create!(service_params_for(project.id, active: true, type: other_type))
-
- expect { subject.perform(project.id, project.id + 1) }.not_to change { services.where(type: other_type).order(:id).map { |row| row.attributes } }
- end
- end
-
- context 'prometheus integration services do not exist' do
- it 'creates missing services entries', :aggregate_failures do
- expect { subject.perform(project.id, project.id + 1) }.to change { services.count }.by(1)
- expect([service_params_for(project.id, active: true)]).to eq services.order(:id).map { |row| row.attributes.slice(*columns).symbolize_keys }
- end
-
- context 'template is present for prometheus services' do
- it 'creates missing services entries', :aggregate_failures do
- services.create!(service_params_for(nil, template: true, properties: { 'from_template' => true }.to_json))
-
- expect { subject.perform(project.id, project.id + 1) }.to change { services.count }.by(1)
- updated_rows = services.where(template: false).order(:id).map { |row| row.attributes.slice(*columns).symbolize_keys }
- expect([service_params_for(project.id, active: true, properties: { 'from_template' => true }.to_json)]).to eq updated_rows
- end
- end
- end
-
- context 'prometheus integration services exist' do
- context 'in active state' do
- it 'does not change them' do
- services.create!(service_params_for(project.id, active: true))
-
- expect { subject.perform(project.id, project.id + 1) }.not_to change { services.order(:id).map { |row| row.attributes } }
- end
- end
-
- context 'not in active state' do
- it 'sets active attribute to true' do
- service = services.create!(service_params_for(project.id, active: false))
-
- expect { subject.perform(project.id, project.id + 1) }.to change { service.reload.active? }.from(false).to(true)
- end
-
- context 'prometheus services are configured manually ' do
- it 'does not change them' do
- properties = '{"api_url":"http://test.dev","manual_configuration":"1"}'
- services.create!(service_params_for(project.id, properties: properties, active: false))
-
- expect { subject.perform(project.id, project.id + 1) }.not_to change { services.order(:id).map { |row| row.attributes } }
- end
- end
- end
- end
- end
-
- context 'k8s cluster shared on instance level' do
- let(:cluster) { clusters.create!(name: 'cluster', cluster_type: cluster_types[:instance_type]) }
-
- context 'with installed prometheus application' do
- before do
- clusters_applications_prometheus.create!(cluster_id: cluster.id, status: application_statuses[:installed], version: '123')
- end
-
- it_behaves_like 'fix services entries state'
- end
-
- context 'with updated prometheus application' do
- before do
- clusters_applications_prometheus.create!(cluster_id: cluster.id, status: application_statuses[:updated], version: '123')
- end
-
- it_behaves_like 'fix services entries state'
- end
-
- context 'with errored prometheus application' do
- before do
- clusters_applications_prometheus.create!(cluster_id: cluster.id, status: application_statuses[:errored], version: '123')
- end
-
- it 'does not change services entries' do
- expect { subject.perform(project.id, project.id + 1) }.not_to change { services.order(:id).map { |row| row.attributes } }
- end
- end
- end
-
- context 'k8s cluster shared on group level' do
- let(:cluster) { clusters.create!(name: 'cluster', cluster_type: cluster_types[:group_type]) }
-
- before do
- cluster_groups.create!(cluster_id: cluster.id, group_id: project.namespace_id)
- end
-
- context 'with installed prometheus application' do
- before do
- clusters_applications_prometheus.create!(cluster_id: cluster.id, status: application_statuses[:installed], version: '123')
- end
-
- it_behaves_like 'fix services entries state'
-
- context 'second k8s cluster without application available' do
- let(:namespace_2) { namespaces.create!(name: 'namespace2', path: 'namespace2') }
- let(:project_2) { projects.create!(namespace_id: namespace_2.id) }
-
- before do
- cluster_2 = clusters.create!(name: 'cluster2', cluster_type: cluster_types[:group_type])
- cluster_groups.create!(cluster_id: cluster_2.id, group_id: project_2.namespace_id)
- end
-
- it 'changed only affected services entries' do
- expect { subject.perform(project.id, project_2.id + 1) }.to change { services.count }.by(1)
- expect([service_params_for(project.id, active: true)]).to eq services.order(:id).map { |row| row.attributes.slice(*columns).symbolize_keys }
- end
- end
- end
-
- context 'with updated prometheus application' do
- before do
- clusters_applications_prometheus.create!(cluster_id: cluster.id, status: application_statuses[:updated], version: '123')
- end
-
- it_behaves_like 'fix services entries state'
- end
-
- context 'with errored prometheus application' do
- before do
- clusters_applications_prometheus.create!(cluster_id: cluster.id, status: application_statuses[:errored], version: '123')
- end
-
- it 'does not change services entries' do
- expect { subject.perform(project.id, project.id + 1) }.not_to change { services.order(:id).map { |row| row.attributes } }
- end
- end
-
- context 'with missing prometheus application' do
- it 'does not change services entries' do
- expect { subject.perform(project.id, project.id + 1) }.not_to change { services.order(:id).map { |row| row.attributes } }
- end
-
- context 'with inactive service' do
- it 'does not change services entries' do
- services.create!(service_params_for(project.id))
-
- expect { subject.perform(project.id, project.id + 1) }.not_to change { services.order(:id).map { |row| row.attributes } }
- end
- end
- end
- end
-
- context 'k8s cluster for single project' do
- let(:cluster) { clusters.create!(name: 'cluster', cluster_type: cluster_types[:project_type]) }
- let(:cluster_projects) { table(:cluster_projects) }
-
- context 'with installed prometheus application' do
- before do
- cluster_projects.create!(cluster_id: cluster.id, project_id: project.id)
- clusters_applications_prometheus.create!(cluster_id: cluster.id, status: application_statuses[:installed], version: '123')
- end
-
- it 'does not change services entries' do
- expect { subject.perform(project.id, project.id + 1) }.not_to change { services.order(:id).map { |row| row.attributes } }
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/background_migration/job_coordinator_spec.rb b/spec/lib/gitlab/background_migration/job_coordinator_spec.rb
new file mode 100644
index 00000000000..a0543ca9958
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/job_coordinator_spec.rb
@@ -0,0 +1,344 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::JobCoordinator do
+ let(:database) { :main }
+ let(:worker_class) { BackgroundMigrationWorker }
+ let(:coordinator) { described_class.new(database, worker_class) }
+
+ describe '.for_database' do
+ it 'returns an executor with the correct worker class and database' do
+ coordinator = described_class.for_database(database)
+
+ expect(coordinator.database).to eq(database)
+ expect(coordinator.worker_class).to eq(worker_class)
+ end
+
+ context 'when passed in as a string' do
+ it 'retruns an executor with the correct worker class and database' do
+ coordinator = described_class.for_database(database.to_s)
+
+ expect(coordinator.database).to eq(database)
+ expect(coordinator.worker_class).to eq(worker_class)
+ end
+ end
+
+ context 'when an invalid value is given' do
+ it 'raises an error' do
+ expect do
+ described_class.for_database('notvalid')
+ end.to raise_error(ArgumentError, "database must be one of [main], got 'notvalid'")
+ end
+ end
+ end
+
+ describe '#queue' do
+ it 'returns background migration worker queue' do
+ expect(coordinator.queue).to eq(worker_class.sidekiq_options['queue'])
+ end
+ end
+
+ describe '#with_shared_connection' do
+ it 'yields to the block after properly configuring SharedModel' do
+ expect(Gitlab::Database::SharedModel).to receive(:using_connection)
+ .with(ActiveRecord::Base.connection).and_yield
+
+ expect { |b| coordinator.with_shared_connection(&b) }.to yield_with_no_args
+ end
+ end
+
+ describe '#steal' do
+ context 'when there are enqueued jobs present' do
+ let(:queue) do
+ [
+ double(args: ['Foo', [10, 20]], klass: worker_class.name),
+ double(args: ['Bar', [20, 30]], klass: worker_class.name),
+ double(args: ['Foo', [20, 30]], klass: 'MergeWorker')
+ ]
+ end
+
+ before do
+ allow(Sidekiq::Queue).to receive(:new)
+ .with(coordinator.queue)
+ .and_return(queue)
+ end
+
+ context 'when queue contains unprocessed jobs' do
+ it 'steals jobs from a queue' do
+ expect(queue[0]).to receive(:delete).and_return(true)
+
+ expect(coordinator).to receive(:perform).with('Foo', [10, 20])
+
+ coordinator.steal('Foo')
+ end
+
+ it 'sets up the shared connection while stealing jobs' do
+ connection = double('connection')
+ allow(coordinator).to receive(:connection).and_return(connection)
+
+ expect(coordinator).to receive(:with_shared_connection).and_call_original
+
+ expect(queue[0]).to receive(:delete).and_return(true)
+
+ expect(coordinator).to receive(:perform).with('Foo', [10, 20]) do
+ expect(Gitlab::Database::SharedModel.connection).to be(connection)
+ end
+
+ coordinator.steal('Foo') do
+ expect(Gitlab::Database::SharedModel.connection).to be(connection)
+
+ true # the job is only performed if the block returns true
+ end
+ end
+
+ it 'does not steal job that has already been taken' do
+ expect(queue[0]).to receive(:delete).and_return(false)
+
+ expect(coordinator).not_to receive(:perform)
+
+ coordinator.steal('Foo')
+ end
+
+ it 'does not steal jobs for a different migration' do
+ expect(coordinator).not_to receive(:perform)
+
+ expect(queue[0]).not_to receive(:delete)
+
+ coordinator.steal('Baz')
+ end
+
+ context 'when a custom predicate is given' do
+ it 'steals jobs that match the predicate' do
+ expect(queue[0]).to receive(:delete).and_return(true)
+
+ expect(coordinator).to receive(:perform).with('Foo', [10, 20])
+
+ coordinator.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
+ expect(described_class).not_to receive(:perform)
+
+ expect(queue[0]).not_to receive(:delete)
+
+ coordinator.steal('Foo') { |(arg1, _)| arg1 == 5 }
+ end
+ end
+ end
+
+ context 'when one of the jobs raises an error' do
+ let(:migration) { spy(:migration) }
+
+ let(:queue) do
+ [double(args: ['Foo', [10, 20]], klass: worker_class.name),
+ double(args: ['Foo', [20, 30]], klass: worker_class.name)]
+ end
+
+ before do
+ stub_const('Gitlab::BackgroundMigration::Foo', migration)
+
+ allow(queue[0]).to receive(:delete).and_return(true)
+ allow(queue[1]).to receive(:delete).and_return(true)
+ end
+
+ it 'enqueues the migration again and re-raises the error' do
+ allow(migration).to receive(:perform).with(10, 20).and_raise(Exception, 'Migration error').once
+
+ expect(worker_class).to receive(:perform_async).with('Foo', [10, 20]).once
+
+ expect { coordinator.steal('Foo') }.to raise_error(Exception)
+ end
+ end
+ end
+
+ context 'when there are scheduled jobs present', :redis do
+ it 'steals all jobs from the scheduled sets' do
+ Sidekiq::Testing.disable! do
+ worker_class.perform_in(10.minutes, 'Object')
+
+ expect(Sidekiq::ScheduledSet.new).to be_one
+ expect(coordinator).to receive(:perform).with('Object', any_args)
+
+ coordinator.steal('Object')
+
+ expect(Sidekiq::ScheduledSet.new).to be_none
+ end
+ end
+ end
+
+ context 'when there are enqueued and scheduled jobs present', :redis do
+ it 'steals from the scheduled sets queue first' do
+ Sidekiq::Testing.disable! do
+ expect(coordinator).to receive(:perform).with('Object', [1]).ordered
+ expect(coordinator).to receive(:perform).with('Object', [2]).ordered
+
+ worker_class.perform_async('Object', [2])
+ worker_class.perform_in(10.minutes, 'Object', [1])
+
+ coordinator.steal('Object')
+ end
+ end
+ end
+
+ context 'when retry_dead_jobs is true', :redis do
+ let(:retry_queue) do
+ [double(args: ['Object', [3]], klass: worker_class.name, delete: true)]
+ end
+
+ let(:dead_queue) do
+ [double(args: ['Object', [4]], klass: worker_class.name, delete: true)]
+ end
+
+ before do
+ allow(Sidekiq::RetrySet).to receive(:new).and_return(retry_queue)
+ allow(Sidekiq::DeadSet).to receive(:new).and_return(dead_queue)
+ end
+
+ it 'steals from the dead and retry queue' do
+ Sidekiq::Testing.disable! do
+ expect(coordinator).to receive(:perform).with('Object', [1]).ordered
+ expect(coordinator).to receive(:perform).with('Object', [2]).ordered
+ expect(coordinator).to receive(:perform).with('Object', [3]).ordered
+ expect(coordinator).to receive(:perform).with('Object', [4]).ordered
+
+ worker_class.perform_async('Object', [2])
+ worker_class.perform_in(10.minutes, 'Object', [1])
+
+ coordinator.steal('Object', retry_dead_jobs: true)
+ end
+ end
+ end
+ end
+
+ describe '#perform' do
+ let(:migration) { spy(:migration) }
+ let(:connection) { double('connection') }
+
+ before do
+ stub_const('Gitlab::BackgroundMigration::Foo', migration)
+
+ allow(coordinator).to receive(:connection).and_return(connection)
+ end
+
+ it 'performs a background migration with the configured shared connection' do
+ expect(coordinator).to receive(:with_shared_connection).and_call_original
+
+ expect(migration).to receive(:perform).with(10, 20).once do
+ expect(Gitlab::Database::SharedModel.connection).to be(connection)
+ end
+
+ coordinator.perform('Foo', [10, 20])
+ end
+ end
+
+ describe '.remaining', :redis do
+ context 'when there are jobs remaining' do
+ before do
+ Sidekiq::Testing.disable! do
+ MergeWorker.perform_async('Foo')
+ MergeWorker.perform_in(10.minutes, 'Foo')
+
+ 5.times do
+ worker_class.perform_async('Foo')
+ end
+ 3.times do
+ worker_class.perform_in(10.minutes, 'Foo')
+ end
+ end
+ end
+
+ it 'returns the enqueued jobs plus the scheduled jobs' do
+ expect(coordinator.remaining).to eq(8)
+ end
+ end
+
+ context 'when there are no jobs remaining' do
+ it 'returns zero' do
+ expect(coordinator.remaining).to be_zero
+ end
+ end
+ end
+
+ describe '.exists?', :redis do
+ context 'when there are enqueued jobs present' do
+ before do
+ Sidekiq::Testing.disable! do
+ MergeWorker.perform_async('Bar')
+ worker_class.perform_async('Foo')
+ end
+ end
+
+ it 'returns true if specific job exists' do
+ expect(coordinator.exists?('Foo')).to eq(true)
+ end
+
+ it 'returns false if specific job does not exist' do
+ expect(coordinator.exists?('Bar')).to eq(false)
+ end
+ end
+
+ context 'when there are scheduled jobs present' do
+ before do
+ Sidekiq::Testing.disable! do
+ MergeWorker.perform_in(10.minutes, 'Bar')
+ worker_class.perform_in(10.minutes, 'Foo')
+ end
+ end
+
+ it 'returns true if specific job exists' do
+ expect(coordinator.exists?('Foo')).to eq(true)
+ end
+
+ it 'returns false if specific job does not exist' do
+ expect(coordinator.exists?('Bar')).to eq(false)
+ end
+ end
+ end
+
+ describe '.dead_jobs?' do
+ let(:queue) do
+ [
+ double(args: ['Foo', [10, 20]], klass: worker_class.name),
+ double(args: ['Bar'], klass: 'MergeWorker')
+ ]
+ end
+
+ context 'when there are dead jobs present' do
+ before do
+ allow(Sidekiq::DeadSet).to receive(:new).and_return(queue)
+ end
+
+ it 'returns true if specific job exists' do
+ expect(coordinator.dead_jobs?('Foo')).to eq(true)
+ end
+
+ it 'returns false if specific job does not exist' do
+ expect(coordinator.dead_jobs?('Bar')).to eq(false)
+ end
+ end
+ end
+
+ describe '.retrying_jobs?' do
+ let(:queue) do
+ [
+ double(args: ['Foo', [10, 20]], klass: worker_class.name),
+ double(args: ['Bar'], klass: 'MergeWorker')
+ ]
+ end
+
+ context 'when there are dead jobs present' do
+ before do
+ allow(Sidekiq::RetrySet).to receive(:new).and_return(queue)
+ end
+
+ it 'returns true if specific job exists' do
+ expect(coordinator.retrying_jobs?('Foo')).to eq(true)
+ end
+
+ it 'returns false if specific job does not exist' do
+ expect(coordinator.retrying_jobs?('Bar')).to eq(false)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/link_lfs_objects_projects_spec.rb b/spec/lib/gitlab/background_migration/link_lfs_objects_projects_spec.rb
index b7cf101dd8a..64e8afedf52 100644
--- a/spec/lib/gitlab/background_migration/link_lfs_objects_projects_spec.rb
+++ b/spec/lib/gitlab/background_migration/link_lfs_objects_projects_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::LinkLfsObjectsProjects, :migration, schema: 2020_03_10_075115 do
+RSpec.describe Gitlab::BackgroundMigration::LinkLfsObjectsProjects, :migration, schema: 20181228175414 do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:fork_networks) { table(:fork_networks) }
diff --git a/spec/lib/gitlab/background_migration/migrate_fingerprint_sha256_within_keys_spec.rb b/spec/lib/gitlab/background_migration/migrate_fingerprint_sha256_within_keys_spec.rb
index c58b2d609e9..4287d6723cf 100644
--- a/spec/lib/gitlab/background_migration/migrate_fingerprint_sha256_within_keys_spec.rb
+++ b/spec/lib/gitlab/background_migration/migrate_fingerprint_sha256_within_keys_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::MigrateFingerprintSha256WithinKeys, schema: 20200106071113 do
+RSpec.describe Gitlab::BackgroundMigration::MigrateFingerprintSha256WithinKeys, schema: 20181228175414 do
subject(:fingerprint_migrator) { described_class.new }
let(:key_table) { table(:keys) }
diff --git a/spec/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data_spec.rb b/spec/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data_spec.rb
deleted file mode 100644
index f2cd2acd4f3..00000000000
--- a/spec/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data_spec.rb
+++ /dev/null
@@ -1,327 +0,0 @@
-# frozen_string_literal: true
-require 'spec_helper'
-
-RSpec.describe Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData, schema: 20200130145430 do
- let(:services) { table(:services) }
-
- before do
- # we need to define the classes due to encryption
- issue_tracker_data = Class.new(ApplicationRecord) do
- self.table_name = 'issue_tracker_data'
-
- def self.encryption_options
- {
- key: Settings.attr_encrypted_db_key_base_32,
- encode: true,
- mode: :per_attribute_iv,
- algorithm: 'aes-256-gcm'
- }
- end
-
- attr_encrypted :project_url, encryption_options
- attr_encrypted :issues_url, encryption_options
- attr_encrypted :new_issue_url, encryption_options
- end
-
- jira_tracker_data = Class.new(ApplicationRecord) do
- self.table_name = 'jira_tracker_data'
-
- def self.encryption_options
- {
- key: Settings.attr_encrypted_db_key_base_32,
- encode: true,
- mode: :per_attribute_iv,
- algorithm: 'aes-256-gcm'
- }
- end
-
- attr_encrypted :url, encryption_options
- attr_encrypted :api_url, encryption_options
- attr_encrypted :username, encryption_options
- attr_encrypted :password, encryption_options
- end
-
- stub_const('IssueTrackerData', issue_tracker_data)
- stub_const('JiraTrackerData', jira_tracker_data)
- end
-
- let(:url) { 'http://base-url.tracker.com' }
- let(:new_issue_url) { 'http://base-url.tracker.com/new_issue' }
- let(:issues_url) { 'http://base-url.tracker.com/issues' }
- let(:api_url) { 'http://api.tracker.com' }
- let(:password) { 'passw1234' }
- let(:username) { 'user9' }
- let(:title) { 'Issue tracker' }
- let(:description) { 'Issue tracker description' }
-
- let(:jira_properties) do
- {
- 'api_url' => api_url,
- 'jira_issue_transition_id' => '5',
- 'password' => password,
- 'url' => url,
- 'username' => username,
- 'title' => title,
- 'description' => description,
- 'other_field' => 'something'
- }
- end
-
- let(:tracker_properties) do
- {
- 'project_url' => url,
- 'new_issue_url' => new_issue_url,
- 'issues_url' => issues_url,
- 'title' => title,
- 'description' => description,
- 'other_field' => 'something'
- }
- end
-
- let(:tracker_properties_no_url) do
- {
- 'new_issue_url' => new_issue_url,
- 'issues_url' => issues_url,
- 'title' => title,
- 'description' => description
- }
- end
-
- subject { described_class.new.perform(1, 100) }
-
- shared_examples 'handle properties' do
- it 'does not clear the properties' do
- expect { subject }.not_to change { service.reload.properties}
- end
- end
-
- context 'with Jira service' do
- let!(:service) do
- services.create!(id: 10, type: 'JiraService', title: nil, properties: jira_properties.to_json, category: 'issue_tracker')
- end
-
- it_behaves_like 'handle properties'
-
- it 'migrates data' do
- expect { subject }.to change { JiraTrackerData.count }.by(1)
-
- service.reload
- data = JiraTrackerData.find_by(service_id: service.id)
-
- expect(data.url).to eq(url)
- expect(data.api_url).to eq(api_url)
- expect(data.username).to eq(username)
- expect(data.password).to eq(password)
- expect(service.title).to eq(title)
- expect(service.description).to eq(description)
- end
- end
-
- context 'with bugzilla service' do
- let!(:service) do
- services.create!(id: 11, type: 'BugzillaService', title: nil, properties: tracker_properties.to_json, category: 'issue_tracker')
- end
-
- it_behaves_like 'handle properties'
-
- it 'migrates data' do
- expect { subject }.to change { IssueTrackerData.count }.by(1)
-
- service.reload
- data = IssueTrackerData.find_by(service_id: service.id)
-
- expect(data.project_url).to eq(url)
- expect(data.issues_url).to eq(issues_url)
- expect(data.new_issue_url).to eq(new_issue_url)
- expect(service.title).to eq(title)
- expect(service.description).to eq(description)
- end
- end
-
- context 'with youtrack service' do
- let!(:service) do
- services.create!(id: 12, type: 'YoutrackService', title: nil, properties: tracker_properties_no_url.to_json, category: 'issue_tracker')
- end
-
- it_behaves_like 'handle properties'
-
- it 'migrates data' do
- expect { subject }.to change { IssueTrackerData.count }.by(1)
-
- service.reload
- data = IssueTrackerData.find_by(service_id: service.id)
-
- expect(data.project_url).to be_nil
- expect(data.issues_url).to eq(issues_url)
- expect(data.new_issue_url).to eq(new_issue_url)
- expect(service.title).to eq(title)
- expect(service.description).to eq(description)
- end
- end
-
- context 'with gitlab service with no properties' do
- let!(:service) do
- services.create!(id: 13, type: 'GitlabIssueTrackerService', title: nil, properties: {}, category: 'issue_tracker')
- end
-
- it_behaves_like 'handle properties'
-
- it 'does not migrate data' do
- expect { subject }.not_to change { IssueTrackerData.count }
- end
- end
-
- context 'with redmine service already with data fields' do
- let!(:service) do
- services.create!(id: 14, type: 'RedmineService', title: nil, properties: tracker_properties_no_url.to_json, category: 'issue_tracker').tap do |service|
- IssueTrackerData.create!(service_id: service.id, project_url: url, new_issue_url: new_issue_url, issues_url: issues_url)
- end
- end
-
- it_behaves_like 'handle properties'
-
- it 'does not create new data fields record' do
- expect { subject }.not_to change { IssueTrackerData.count }
- end
- end
-
- context 'with custom issue tracker which has data fields record inconsistent with properties field' do
- let!(:service) do
- services.create!(id: 15, type: 'CustomIssueTrackerService', title: 'Existing title', properties: jira_properties.to_json, category: 'issue_tracker').tap do |service|
- IssueTrackerData.create!(service_id: service.id, project_url: 'http://other_url', new_issue_url: 'http://other_url/new_issue', issues_url: 'http://other_url/issues')
- end
- end
-
- it_behaves_like 'handle properties'
-
- it 'does not update the data fields record' do
- expect { subject }.not_to change { IssueTrackerData.count }
-
- service.reload
- data = IssueTrackerData.find_by(service_id: service.id)
-
- expect(data.project_url).to eq('http://other_url')
- expect(data.issues_url).to eq('http://other_url/issues')
- expect(data.new_issue_url).to eq('http://other_url/new_issue')
- expect(service.title).to eq('Existing title')
- end
- end
-
- context 'with Jira service which has data fields record inconsistent with properties field' do
- let!(:service) do
- services.create!(id: 16, type: 'CustomIssueTrackerService', description: 'Existing description', properties: jira_properties.to_json, category: 'issue_tracker').tap do |service|
- JiraTrackerData.create!(service_id: service.id, url: 'http://other_jira_url')
- end
- end
-
- it_behaves_like 'handle properties'
-
- it 'does not update the data fields record' do
- expect { subject }.not_to change { JiraTrackerData.count }
-
- service.reload
- data = JiraTrackerData.find_by(service_id: service.id)
-
- expect(data.url).to eq('http://other_jira_url')
- expect(data.password).to be_nil
- expect(data.username).to be_nil
- expect(data.api_url).to be_nil
- expect(service.description).to eq('Existing description')
- end
- end
-
- context 'non issue tracker service' do
- let!(:service) do
- services.create!(id: 17, title: nil, description: nil, type: 'OtherService', properties: tracker_properties.to_json)
- end
-
- it_behaves_like 'handle properties'
-
- it 'does not migrate any data' do
- expect { subject }.not_to change { IssueTrackerData.count }
-
- service.reload
- expect(service.title).to be_nil
- expect(service.description).to be_nil
- end
- end
-
- context 'Jira service with empty properties' do
- let!(:service) do
- services.create!(id: 18, type: 'JiraService', properties: '', category: 'issue_tracker')
- end
-
- it_behaves_like 'handle properties'
-
- it 'does not migrate any data' do
- expect { subject }.not_to change { JiraTrackerData.count }
- end
- end
-
- context 'Jira service with nil properties' do
- let!(:service) do
- services.create!(id: 18, type: 'JiraService', properties: nil, category: 'issue_tracker')
- end
-
- it_behaves_like 'handle properties'
-
- it 'does not migrate any data' do
- expect { subject }.not_to change { JiraTrackerData.count }
- end
- end
-
- context 'Jira service with invalid properties' do
- let!(:service) do
- services.create!(id: 18, type: 'JiraService', properties: 'invalid data', category: 'issue_tracker')
- end
-
- it_behaves_like 'handle properties'
-
- it 'does not migrate any data' do
- expect { subject }.not_to change { JiraTrackerData.count }
- end
- end
-
- context 'with Jira service with invalid properties, valid Jira service and valid bugzilla service' do
- let!(:jira_integration_invalid) do
- services.create!(id: 19, title: 'invalid - title', description: 'invalid - description', type: 'JiraService', properties: 'invalid data', category: 'issue_tracker')
- end
-
- let!(:jira_integration_valid) do
- services.create!(id: 20, type: 'JiraService', properties: jira_properties.to_json, category: 'issue_tracker')
- end
-
- let!(:bugzilla_integration_valid) do
- services.create!(id: 11, type: 'BugzillaService', title: nil, properties: tracker_properties.to_json, category: 'issue_tracker')
- end
-
- it 'migrates data for the valid service' do
- subject
-
- jira_integration_invalid.reload
- expect(JiraTrackerData.find_by(service_id: jira_integration_invalid.id)).to be_nil
- expect(jira_integration_invalid.title).to eq('invalid - title')
- expect(jira_integration_invalid.description).to eq('invalid - description')
- expect(jira_integration_invalid.properties).to eq('invalid data')
-
- jira_integration_valid.reload
- data = JiraTrackerData.find_by(service_id: jira_integration_valid.id)
-
- expect(data.url).to eq(url)
- expect(data.api_url).to eq(api_url)
- expect(data.username).to eq(username)
- expect(data.password).to eq(password)
- expect(jira_integration_valid.title).to eq(title)
- expect(jira_integration_valid.description).to eq(description)
-
- bugzilla_integration_valid.reload
- data = IssueTrackerData.find_by(service_id: bugzilla_integration_valid.id)
-
- expect(data.project_url).to eq(url)
- expect(data.issues_url).to eq(issues_url)
- expect(data.new_issue_url).to eq(new_issue_url)
- expect(bugzilla_integration_valid.title).to eq(title)
- expect(bugzilla_integration_valid.description).to eq(description)
- end
- end
-end
diff --git a/spec/lib/gitlab/background_migration/migrate_merge_request_diff_commit_users_spec.rb b/spec/lib/gitlab/background_migration/migrate_merge_request_diff_commit_users_spec.rb
index 91e8dcdf880..31b6ee0c7cd 100644
--- a/spec/lib/gitlab/background_migration/migrate_merge_request_diff_commit_users_spec.rb
+++ b/spec/lib/gitlab/background_migration/migrate_merge_request_diff_commit_users_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::MigrateMergeRequestDiffCommitUsers do
+RSpec.describe Gitlab::BackgroundMigration::MigrateMergeRequestDiffCommitUsers, schema: 20211012134316 do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:users) { table(:users) }
diff --git a/spec/lib/gitlab/background_migration/migrate_u2f_webauthn_spec.rb b/spec/lib/gitlab/background_migration/migrate_u2f_webauthn_spec.rb
index 9eda51f6ec4..ab183d01357 100644
--- a/spec/lib/gitlab/background_migration/migrate_u2f_webauthn_spec.rb
+++ b/spec/lib/gitlab/background_migration/migrate_u2f_webauthn_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require 'webauthn/u2f_migrator'
-RSpec.describe Gitlab::BackgroundMigration::MigrateU2fWebauthn, :migration, schema: 20200925125321 do
+RSpec.describe Gitlab::BackgroundMigration::MigrateU2fWebauthn, :migration, schema: 20181228175414 do
let(:users) { table(:users) }
let(:user) { users.create!(email: 'email@email.com', name: 'foo', username: 'foo', projects_limit: 0) }
diff --git a/spec/lib/gitlab/background_migration/migrate_users_bio_to_user_details_spec.rb b/spec/lib/gitlab/background_migration/migrate_users_bio_to_user_details_spec.rb
deleted file mode 100644
index d90a5d30954..00000000000
--- a/spec/lib/gitlab/background_migration/migrate_users_bio_to_user_details_spec.rb
+++ /dev/null
@@ -1,85 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::BackgroundMigration::MigrateUsersBioToUserDetails, :migration, schema: 20200323074147 do
- let(:users) { table(:users) }
-
- let(:user_details) do
- klass = table(:user_details)
- klass.primary_key = :user_id
- klass
- end
-
- let!(:user_needs_migration) { users.create!(name: 'user1', email: 'test1@test.com', projects_limit: 1, bio: 'bio') }
- let!(:user_needs_no_migration) { users.create!(name: 'user2', email: 'test2@test.com', projects_limit: 1) }
- let!(:user_also_needs_no_migration) { users.create!(name: 'user3', email: 'test3@test.com', projects_limit: 1, bio: '') }
- let!(:user_with_long_bio) { users.create!(name: 'user4', email: 'test4@test.com', projects_limit: 1, bio: 'a' * 256) } # 255 is the max
-
- let!(:user_already_has_details) { users.create!(name: 'user5', email: 'test5@test.com', projects_limit: 1, bio: 'my bio') }
- let!(:existing_user_details) { user_details.find_or_create_by!(user_id: user_already_has_details.id).update!(bio: 'my bio') }
-
- # unlikely scenario since we have triggers
- let!(:user_has_different_details) { users.create!(name: 'user6', email: 'test6@test.com', projects_limit: 1, bio: 'different') }
- let!(:different_existing_user_details) { user_details.find_or_create_by!(user_id: user_has_different_details.id).update!(bio: 'bio') }
-
- let(:user_ids) do
- [
- user_needs_migration,
- user_needs_no_migration,
- user_also_needs_no_migration,
- user_with_long_bio,
- user_already_has_details,
- user_has_different_details
- ].map(&:id)
- end
-
- subject { described_class.new.perform(user_ids.min, user_ids.max) }
-
- it 'migrates all relevant records' do
- subject
-
- all_user_details = user_details.all
- expect(all_user_details.size).to eq(4)
- end
-
- it 'migrates `bio`' do
- subject
-
- user_detail = user_details.find_by!(user_id: user_needs_migration.id)
-
- expect(user_detail.bio).to eq('bio')
- end
-
- it 'migrates long `bio`' do
- subject
-
- user_detail = user_details.find_by!(user_id: user_with_long_bio.id)
-
- expect(user_detail.bio).to eq('a' * 255)
- end
-
- it 'does not change existing user detail' do
- expect { subject }.not_to change { user_details.find_by!(user_id: user_already_has_details.id).attributes }
- end
-
- it 'changes existing user detail when the columns are different' do
- expect { subject }.to change { user_details.find_by!(user_id: user_has_different_details.id).bio }.from('bio').to('different')
- end
-
- it 'does not migrate record' do
- subject
-
- user_detail = user_details.find_by(user_id: user_needs_no_migration.id)
-
- expect(user_detail).to be_nil
- end
-
- it 'does not migrate empty bio' do
- subject
-
- user_detail = user_details.find_by(user_id: user_also_needs_no_migration.id)
-
- expect(user_detail).to be_nil
- end
-end
diff --git a/spec/lib/gitlab/background_migration/populate_canonical_emails_spec.rb b/spec/lib/gitlab/background_migration/populate_canonical_emails_spec.rb
index 36000dc3ffd..944ee98ed4a 100644
--- a/spec/lib/gitlab/background_migration/populate_canonical_emails_spec.rb
+++ b/spec/lib/gitlab/background_migration/populate_canonical_emails_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::PopulateCanonicalEmails, :migration, schema: 20200312053852 do
+RSpec.describe Gitlab::BackgroundMigration::PopulateCanonicalEmails, :migration, schema: 20181228175414 do
let(:migration) { described_class.new }
let_it_be(:users_table) { table(:users) }
diff --git a/spec/lib/gitlab/background_migration/populate_dismissed_state_for_vulnerabilities_spec.rb b/spec/lib/gitlab/background_migration/populate_dismissed_state_for_vulnerabilities_spec.rb
index bc55f240a58..dc8c8c75b83 100644
--- a/spec/lib/gitlab/background_migration/populate_dismissed_state_for_vulnerabilities_spec.rb
+++ b/spec/lib/gitlab/background_migration/populate_dismissed_state_for_vulnerabilities_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Gitlab::BackgroundMigration::PopulateDismissedStateForVulnerabilities, schema: 2020_11_30_103926 do
+RSpec.describe ::Gitlab::BackgroundMigration::PopulateDismissedStateForVulnerabilities, schema: 20181228175414 do
let(:users) { table(:users) }
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
diff --git a/spec/lib/gitlab/background_migration/populate_finding_uuid_for_vulnerability_feedback_spec.rb b/spec/lib/gitlab/background_migration/populate_finding_uuid_for_vulnerability_feedback_spec.rb
index 07b1d99d333..25006e663ab 100644
--- a/spec/lib/gitlab/background_migration/populate_finding_uuid_for_vulnerability_feedback_spec.rb
+++ b/spec/lib/gitlab/background_migration/populate_finding_uuid_for_vulnerability_feedback_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::PopulateFindingUuidForVulnerabilityFeedback, schema: 20201211090634 do
+RSpec.describe Gitlab::BackgroundMigration::PopulateFindingUuidForVulnerabilityFeedback, schema: 20181228175414 do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:users) { table(:users) }
diff --git a/spec/lib/gitlab/background_migration/populate_has_vulnerabilities_spec.rb b/spec/lib/gitlab/background_migration/populate_has_vulnerabilities_spec.rb
index c6385340ca3..6722321d5f7 100644
--- a/spec/lib/gitlab/background_migration/populate_has_vulnerabilities_spec.rb
+++ b/spec/lib/gitlab/background_migration/populate_has_vulnerabilities_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::PopulateHasVulnerabilities, schema: 20201103192526 do
+RSpec.describe Gitlab::BackgroundMigration::PopulateHasVulnerabilities, schema: 20181228175414 do
let(:users) { table(:users) }
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
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
index f724b007e01..a03a11489b5 100644
--- a/spec/lib/gitlab/background_migration/populate_issue_email_participants_spec.rb
+++ b/spec/lib/gitlab/background_migration/populate_issue_email_participants_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::PopulateIssueEmailParticipants, schema: 20201128210234 do
+RSpec.describe Gitlab::BackgroundMigration::PopulateIssueEmailParticipants, schema: 20181228175414 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") }
diff --git a/spec/lib/gitlab/background_migration/populate_missing_vulnerability_dismissal_information_spec.rb b/spec/lib/gitlab/background_migration/populate_missing_vulnerability_dismissal_information_spec.rb
index 44c5f3d1381..1c987d3876f 100644
--- a/spec/lib/gitlab/background_migration/populate_missing_vulnerability_dismissal_information_spec.rb
+++ b/spec/lib/gitlab/background_migration/populate_missing_vulnerability_dismissal_information_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::PopulateMissingVulnerabilityDismissalInformation, schema: 20201028160832 do
+RSpec.describe Gitlab::BackgroundMigration::PopulateMissingVulnerabilityDismissalInformation, schema: 20181228175414 do
let(:users) { table(:users) }
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
diff --git a/spec/lib/gitlab/background_migration/populate_personal_snippet_statistics_spec.rb b/spec/lib/gitlab/background_migration/populate_personal_snippet_statistics_spec.rb
index e746451b1b9..f9628849dbf 100644
--- a/spec/lib/gitlab/background_migration/populate_personal_snippet_statistics_spec.rb
+++ b/spec/lib/gitlab/background_migration/populate_personal_snippet_statistics_spec.rb
@@ -111,11 +111,11 @@ RSpec.describe Gitlab::BackgroundMigration::PopulatePersonalSnippetStatistics do
if with_repo
allow(snippet).to receive(:disk_path).and_return(disk_path(snippet))
+ raw_repository(snippet).create_repository
+
TestEnv.copy_repo(snippet,
bare_repo: TestEnv.factory_repo_path_bare,
refs: TestEnv::BRANCH_SHA)
-
- raw_repository(snippet).create_repository
end
end
end
diff --git a/spec/lib/gitlab/background_migration/populate_project_snippet_statistics_spec.rb b/spec/lib/gitlab/background_migration/populate_project_snippet_statistics_spec.rb
index 897f5e81372..7884e0d97c0 100644
--- a/spec/lib/gitlab/background_migration/populate_project_snippet_statistics_spec.rb
+++ b/spec/lib/gitlab/background_migration/populate_project_snippet_statistics_spec.rb
@@ -183,11 +183,11 @@ RSpec.describe Gitlab::BackgroundMigration::PopulateProjectSnippetStatistics do
if with_repo
allow(snippet).to receive(:disk_path).and_return(disk_path(snippet))
+ raw_repository(snippet).create_repository
+
TestEnv.copy_repo(snippet,
bare_repo: TestEnv.factory_repo_path_bare,
refs: TestEnv::BRANCH_SHA)
-
- raw_repository(snippet).create_repository
end
end
end
diff --git a/spec/lib/gitlab/background_migration/populate_user_highest_roles_table_spec.rb b/spec/lib/gitlab/background_migration/populate_user_highest_roles_table_spec.rb
deleted file mode 100644
index b3cacc60cdc..00000000000
--- a/spec/lib/gitlab/background_migration/populate_user_highest_roles_table_spec.rb
+++ /dev/null
@@ -1,71 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::BackgroundMigration::PopulateUserHighestRolesTable, schema: 20200311130802 do
- let(:members) { table(:members) }
- let(:users) { table(:users) }
- let(:user_highest_roles) { table(:user_highest_roles) }
-
- def create_user(id, params = {})
- user_params = {
- id: id,
- state: 'active',
- user_type: nil,
- bot_type: nil,
- ghost: nil,
- email: "user#{id}@example.com",
- projects_limit: 0
- }.merge(params)
-
- users.create!(user_params)
- end
-
- def create_member(id, access_level, params = {})
- params = {
- user_id: id,
- access_level: access_level,
- source_id: 1,
- source_type: 'Group',
- notification_level: 0
- }.merge(params)
-
- members.create!(params)
- end
-
- before do
- create_user(1)
- create_user(2, state: 'blocked')
- create_user(3, user_type: 2)
- create_user(4)
- create_user(5, bot_type: 1)
- create_user(6, ghost: true)
- create_user(7, ghost: false)
- create_user(8)
-
- create_member(1, 40)
- create_member(7, 30)
- create_member(8, 20, requested_at: Time.current)
-
- user_highest_roles.create!(user_id: 1, highest_access_level: 50)
- end
-
- describe '#perform' do
- it 'creates user_highest_roles rows according to users', :aggregate_failures do
- expect { subject.perform(1, 8) }.to change(UserHighestRole, :count).from(1).to(4)
-
- created_or_updated_rows = [
- { 'user_id' => 1, 'highest_access_level' => 40 },
- { 'user_id' => 4, 'highest_access_level' => nil },
- { 'user_id' => 7, 'highest_access_level' => 30 },
- { 'user_id' => 8, 'highest_access_level' => nil }
- ]
-
- rows = user_highest_roles.order(:user_id).map do |row|
- row.attributes.slice('user_id', 'highest_access_level')
- end
-
- expect(rows).to match_array(created_or_updated_rows)
- end
- end
-end
diff --git a/spec/lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces_spec.rb b/spec/lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces_spec.rb
new file mode 100644
index 00000000000..24259b06469
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces_spec.rb
@@ -0,0 +1,254 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::ProjectNamespaces::BackfillProjectNamespaces, :migration do
+ include MigrationsHelpers
+
+ context 'when migrating data', :aggregate_failures do
+ let(:projects) { table(:projects) }
+ let(:namespaces) { table(:namespaces) }
+
+ let(:parent_group1) { namespaces.create!(name: 'parent_group1', path: 'parent_group1', visibility_level: 20, type: 'Group') }
+ let(:parent_group2) { namespaces.create!(name: 'test1', path: 'test1', runners_token: 'my-token1', project_creation_level: 1, visibility_level: 20, type: 'Group') }
+
+ let(:parent_group1_project) { projects.create!(name: 'parent_group1_project', path: 'parent_group1_project', namespace_id: parent_group1.id, visibility_level: 20) }
+ let(:parent_group2_project) { projects.create!(name: 'parent_group2_project', path: 'parent_group2_project', namespace_id: parent_group2.id, visibility_level: 20) }
+
+ let(:child_nodes_count) { 2 }
+ let(:tree_depth) { 3 }
+
+ let(:backfilled_namespace) { nil }
+
+ before do
+ BackfillProjectNamespaces::TreeGenerator.new(namespaces, projects, [parent_group1, parent_group2], child_nodes_count, tree_depth).build_tree
+ end
+
+ describe '#up' do
+ shared_examples 'back-fill project namespaces' do
+ it 'back-fills all project namespaces' do
+ start_id = ::Project.minimum(:id)
+ end_id = ::Project.maximum(:id)
+ projects_count = ::Project.count
+ batches_count = (projects_count / described_class::BATCH_SIZE.to_f).ceil
+ project_namespaces_count = ::Namespace.where(type: 'Project').count
+ migration = described_class.new
+
+ expect(projects_count).not_to eq(project_namespaces_count)
+ expect(migration).to receive(:batch_insert_namespaces).exactly(batches_count).and_call_original
+ expect(migration).to receive(:batch_update_projects).exactly(batches_count).and_call_original
+ expect(migration).to receive(:batch_update_project_namespaces_traversal_ids).exactly(batches_count).and_call_original
+
+ expect { migration.perform(start_id, end_id, nil, 'up') }.to change(Namespace.where(type: 'Project'), :count)
+
+ expect(projects_count).to eq(::Namespace.where(type: 'Project').count)
+ check_projects_in_sync_with(Namespace.where(type: 'Project'))
+ end
+
+ context 'when passing specific group as parameter' do
+ let(:backfilled_namespace) { parent_group1 }
+
+ it 'back-fills project namespaces for the specified group hierarchy' do
+ backfilled_namespace_projects = base_ancestor(backfilled_namespace).first.all_projects
+ start_id = backfilled_namespace_projects.minimum(:id)
+ end_id = backfilled_namespace_projects.maximum(:id)
+ group_projects_count = backfilled_namespace_projects.count
+ batches_count = (group_projects_count / described_class::BATCH_SIZE.to_f).ceil
+ project_namespaces_in_hierarchy = project_namespaces_in_hierarchy(base_ancestor(backfilled_namespace))
+
+ migration = described_class.new
+
+ expect(project_namespaces_in_hierarchy.count).to eq(0)
+ expect(migration).to receive(:batch_insert_namespaces).exactly(batches_count).and_call_original
+ expect(migration).to receive(:batch_update_projects).exactly(batches_count).and_call_original
+ expect(migration).to receive(:batch_update_project_namespaces_traversal_ids).exactly(batches_count).and_call_original
+
+ expect(group_projects_count).to eq(14)
+ expect(project_namespaces_in_hierarchy.count).to eq(0)
+
+ migration.perform(start_id, end_id, backfilled_namespace.id, 'up')
+
+ expect(project_namespaces_in_hierarchy.count).to eq(14)
+ check_projects_in_sync_with(project_namespaces_in_hierarchy)
+ end
+ end
+
+ context 'when projects already have project namespaces' do
+ before do
+ hierarchy1_projects = base_ancestor(parent_group1).first.all_projects
+ start_id = hierarchy1_projects.minimum(:id)
+ end_id = hierarchy1_projects.maximum(:id)
+
+ described_class.new.perform(start_id, end_id, parent_group1.id, 'up')
+ end
+
+ it 'does not duplicate project namespaces' do
+ # check there are already some project namespaces but not for all
+ projects_count = ::Project.count
+ start_id = ::Project.minimum(:id)
+ end_id = ::Project.maximum(:id)
+ batches_count = (projects_count / described_class::BATCH_SIZE.to_f).ceil
+ project_namespaces = ::Namespace.where(type: 'Project')
+ migration = described_class.new
+
+ expect(project_namespaces_in_hierarchy(base_ancestor(parent_group1)).count).to be >= 14
+ expect(project_namespaces_in_hierarchy(base_ancestor(parent_group2)).count).to eq(0)
+ expect(projects_count).not_to eq(project_namespaces.count)
+
+ # run migration again to test we do not generate extra project namespaces
+ expect(migration).to receive(:batch_insert_namespaces).exactly(batches_count).and_call_original
+ expect(migration).to receive(:batch_update_projects).exactly(batches_count).and_call_original
+ expect(migration).to receive(:batch_update_project_namespaces_traversal_ids).exactly(batches_count).and_call_original
+
+ expect { migration.perform(start_id, end_id, nil, 'up') }.to change(project_namespaces, :count).by(14)
+
+ expect(projects_count).to eq(project_namespaces.count)
+ end
+ end
+ end
+
+ it 'checks no project namespaces exist in the defined hierarchies' do
+ hierarchy1_project_namespaces = project_namespaces_in_hierarchy(base_ancestor(parent_group1))
+ hierarchy2_project_namespaces = project_namespaces_in_hierarchy(base_ancestor(parent_group2))
+ hierarchy1_projects_count = base_ancestor(parent_group1).first.all_projects.count
+ hierarchy2_projects_count = base_ancestor(parent_group2).first.all_projects.count
+
+ expect(hierarchy1_project_namespaces).to be_empty
+ expect(hierarchy2_project_namespaces).to be_empty
+ expect(hierarchy1_projects_count).to eq(14)
+ expect(hierarchy2_projects_count).to eq(14)
+ end
+
+ context 'back-fill project namespaces in a single batch' do
+ it_behaves_like 'back-fill project namespaces'
+ end
+
+ context 'back-fill project namespaces in batches' do
+ before do
+ stub_const("#{described_class.name}::BATCH_SIZE", 2)
+ end
+
+ it_behaves_like 'back-fill project namespaces'
+ end
+ end
+
+ describe '#down' do
+ before do
+ start_id = ::Project.minimum(:id)
+ end_id = ::Project.maximum(:id)
+ # back-fill first
+ described_class.new.perform(start_id, end_id, nil, 'up')
+ end
+
+ shared_examples 'cleanup project namespaces' do
+ it 'removes project namespaces' do
+ projects_count = ::Project.count
+ start_id = ::Project.minimum(:id)
+ end_id = ::Project.maximum(:id)
+ migration = described_class.new
+ batches_count = (projects_count / described_class::BATCH_SIZE.to_f).ceil
+
+ expect(projects_count).to be > 0
+ expect(projects_count).to eq(::Namespace.where(type: 'Project').count)
+
+ expect(migration).to receive(:nullify_project_namespaces_in_projects).exactly(batches_count).and_call_original
+ expect(migration).to receive(:delete_project_namespace_records).exactly(batches_count).and_call_original
+
+ migration.perform(start_id, end_id, nil, 'down')
+
+ expect(::Project.count).to be > 0
+ expect(::Namespace.where(type: 'Project').count).to eq(0)
+ end
+
+ context 'when passing specific group as parameter' do
+ let(:backfilled_namespace) { parent_group1 }
+
+ it 'removes project namespaces only for the specific group hierarchy' do
+ backfilled_namespace_projects = base_ancestor(backfilled_namespace).first.all_projects
+ start_id = backfilled_namespace_projects.minimum(:id)
+ end_id = backfilled_namespace_projects.maximum(:id)
+ group_projects_count = backfilled_namespace_projects.count
+ batches_count = (group_projects_count / described_class::BATCH_SIZE.to_f).ceil
+ project_namespaces_in_hierarchy = project_namespaces_in_hierarchy(base_ancestor(backfilled_namespace))
+ migration = described_class.new
+
+ expect(project_namespaces_in_hierarchy.count).to eq(14)
+ expect(migration).to receive(:nullify_project_namespaces_in_projects).exactly(batches_count).and_call_original
+ expect(migration).to receive(:delete_project_namespace_records).exactly(batches_count).and_call_original
+
+ migration.perform(start_id, end_id, backfilled_namespace.id, 'down')
+
+ expect(::Namespace.where(type: 'Project').count).to be > 0
+ expect(project_namespaces_in_hierarchy.count).to eq(0)
+ end
+ end
+ end
+
+ context 'cleanup project namespaces in a single batch' do
+ it_behaves_like 'cleanup project namespaces'
+ end
+
+ context 'cleanup project namespaces in batches' do
+ before do
+ stub_const("#{described_class.name}::BATCH_SIZE", 2)
+ end
+
+ it_behaves_like 'cleanup project namespaces'
+ end
+ end
+ end
+
+ def base_ancestor(ancestor)
+ ::Namespace.where(id: ancestor.id)
+ end
+
+ def project_namespaces_in_hierarchy(base_node)
+ Gitlab::ObjectHierarchy.new(base_node).base_and_descendants.where(type: 'Project')
+ end
+
+ def check_projects_in_sync_with(namespaces)
+ project_namespaces_attrs = namespaces.order(:id).pluck(:id, :name, :path, :parent_id, :visibility_level, :shared_runners_enabled)
+ corresponding_projects_attrs = Project.where(project_namespace_id: project_namespaces_attrs.map(&:first))
+ .order(:project_namespace_id).pluck(:project_namespace_id, :name, :path, :namespace_id, :visibility_level, :shared_runners_enabled)
+
+ expect(project_namespaces_attrs).to eq(corresponding_projects_attrs)
+ end
+end
+
+module BackfillProjectNamespaces
+ class TreeGenerator
+ def initialize(namespaces, projects, parent_nodes, child_nodes_count, tree_depth)
+ parent_nodes_ids = parent_nodes.map(&:id)
+
+ @namespaces = namespaces
+ @projects = projects
+ @subgroups_depth = tree_depth
+ @resource_count = child_nodes_count
+ @all_groups = [parent_nodes_ids]
+ end
+
+ def build_tree
+ (1..@subgroups_depth).each do |level|
+ parent_level = level - 1
+ current_level = level
+ parent_groups = @all_groups[parent_level]
+
+ parent_groups.each do |parent_id|
+ @resource_count.times do |i|
+ group_path = "child#{i}_level#{level}"
+ project_path = "project#{i}_level#{level}"
+ sub_group = @namespaces.create!(name: group_path, path: group_path, parent_id: parent_id, visibility_level: 20, type: 'Group')
+ @projects.create!(name: project_path, path: project_path, namespace_id: sub_group.id, visibility_level: 20)
+
+ track_group_id(current_level, sub_group.id)
+ end
+ end
+ end
+ end
+
+ def track_group_id(depth_level, group_id)
+ @all_groups[depth_level] ||= []
+ @all_groups[depth_level] << group_id
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/recalculate_project_authorizations_with_min_max_user_id_spec.rb b/spec/lib/gitlab/background_migration/recalculate_project_authorizations_with_min_max_user_id_spec.rb
index c1ba1607b89..1830a7fc099 100644
--- a/spec/lib/gitlab/background_migration/recalculate_project_authorizations_with_min_max_user_id_spec.rb
+++ b/spec/lib/gitlab/background_migration/recalculate_project_authorizations_with_min_max_user_id_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::RecalculateProjectAuthorizationsWithMinMaxUserId, schema: 20200204113224 do
+RSpec.describe Gitlab::BackgroundMigration::RecalculateProjectAuthorizationsWithMinMaxUserId, schema: 20181228175414 do
let(:users_table) { table(:users) }
let(:min) { 1 }
let(:max) { 5 }
diff --git a/spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb b/spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb
index 30908145782..4cdb56d3d3b 100644
--- a/spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb
+++ b/spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrencesUuid, schema: 20201110110454 do
+RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrencesUuid, schema: 20181228175414 do
let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
let(:users) { table(:users) }
let(:user) { create_user! }
diff --git a/spec/lib/gitlab/background_migration/remove_duplicate_services_spec.rb b/spec/lib/gitlab/background_migration/remove_duplicate_services_spec.rb
index 391b27b28e6..afcdaaf1cb8 100644
--- a/spec/lib/gitlab/background_migration/remove_duplicate_services_spec.rb
+++ b/spec/lib/gitlab/background_migration/remove_duplicate_services_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::RemoveDuplicateServices, :migration, schema: 20201207165956 do
+RSpec.describe Gitlab::BackgroundMigration::RemoveDuplicateServices, :migration, schema: 20181228175414 do
let_it_be(:users) { table(:users) }
let_it_be(:namespaces) { table(:namespaces) }
let_it_be(:projects) { table(:projects) }
diff --git a/spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb b/spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb
index 47e1d4620cd..7214225c32c 100644
--- a/spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb
+++ b/spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb
@@ -5,9 +5,9 @@ RSpec.describe Gitlab::BackgroundMigration::RemoveDuplicateVulnerabilitiesFindin
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(:project) { table(:projects).create!(id: 14219619, 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!(:scanner1) { scanners.create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
let!(:scanner2) { scanners.create!(project_id: project.id, external_id: 'test 2', name: 'test scanner 2') }
let!(:scanner3) { scanners.create!(project_id: project.id, external_id: 'test 3', name: 'test scanner 3') }
let!(:unrelated_scanner) { scanners.create!(project_id: project.id, external_id: 'unreleated_scanner', name: 'unrelated scanner') }
@@ -16,43 +16,68 @@ RSpec.describe Gitlab::BackgroundMigration::RemoveDuplicateVulnerabilitiesFindin
let(:vulnerability_identifiers) { table(:vulnerability_identifiers) }
let(:vulnerability_identifier) do
vulnerability_identifiers.create!(
+ id: 1244459,
project_id: project.id,
external_type: 'vulnerability-identifier',
external_id: 'vulnerability-identifier',
- fingerprint: '7e394d1b1eb461a7406d7b1e08f057a1cf11287a',
+ fingerprint: '0a203e8cd5260a1948edbedc76c7cb91ad6a2e45',
name: 'vulnerability identifier')
end
- let!(:first_finding) do
+ let!(:vulnerability_for_first_duplicate) do
+ create_vulnerability!(
+ project_id: project.id,
+ author_id: user.id
+ )
+ end
+
+ let!(:first_finding_duplicate) do
create_finding!(
- uuid: "test1",
- vulnerability_id: nil,
+ id: 5606961,
+ uuid: "bd95c085-71aa-51d7-9bb6-08ae669c262e",
+ vulnerability_id: vulnerability_for_first_duplicate.id,
report_type: 0,
- location_fingerprint: '2bda3014914481791847d8eca38d1a8d13b6ad76',
+ location_fingerprint: '00049d5119c2cb3bfb3d1ee1f6e031fe925aed75',
primary_identifier_id: vulnerability_identifier.id,
- scanner_id: scanner.id,
+ scanner_id: scanner1.id,
project_id: project.id
)
end
- let!(:first_duplicate) do
+ let!(:vulnerability_for_second_duplicate) do
+ create_vulnerability!(
+ project_id: project.id,
+ author_id: user.id
+ )
+ end
+
+ let!(:second_finding_duplicate) do
create_finding!(
- uuid: "test2",
- vulnerability_id: nil,
+ id: 8765432,
+ uuid: "5b714f58-1176-5b26-8fd5-e11dfcb031b5",
+ vulnerability_id: vulnerability_for_second_duplicate.id,
report_type: 0,
- location_fingerprint: '2bda3014914481791847d8eca38d1a8d13b6ad76',
+ location_fingerprint: '00049d5119c2cb3bfb3d1ee1f6e031fe925aed75',
primary_identifier_id: vulnerability_identifier.id,
scanner_id: scanner2.id,
project_id: project.id
)
end
- let!(:second_duplicate) do
+ let!(:vulnerability_for_third_duplicate) do
+ create_vulnerability!(
+ project_id: project.id,
+ author_id: user.id
+ )
+ end
+
+ let!(:third_finding_duplicate) do
create_finding!(
- uuid: "test3",
- vulnerability_id: nil,
+ id: 8832995,
+ uuid: "cfe435fa-b25b-5199-a56d-7b007cc9e2d4",
+ vulnerability_id: vulnerability_for_third_duplicate.id,
report_type: 0,
- location_fingerprint: '2bda3014914481791847d8eca38d1a8d13b6ad76',
+ location_fingerprint: '00049d5119c2cb3bfb3d1ee1f6e031fe925aed75',
primary_identifier_id: vulnerability_identifier.id,
scanner_id: scanner3.id,
project_id: project.id
@@ -61,6 +86,7 @@ RSpec.describe Gitlab::BackgroundMigration::RemoveDuplicateVulnerabilitiesFindin
let!(:unrelated_finding) do
create_finding!(
+ id: 9999999,
uuid: "unreleated_finding",
vulnerability_id: nil,
report_type: 1,
@@ -71,7 +97,7 @@ RSpec.describe Gitlab::BackgroundMigration::RemoveDuplicateVulnerabilitiesFindin
)
end
- subject { described_class.new.perform(first_finding.id, unrelated_finding.id) }
+ subject { described_class.new.perform(first_finding_duplicate.id, unrelated_finding.id) }
before do
stub_const("#{described_class}::DELETE_BATCH_SIZE", 1)
@@ -82,7 +108,15 @@ RSpec.describe Gitlab::BackgroundMigration::RemoveDuplicateVulnerabilitiesFindin
expect { subject }.to change { vulnerability_findings.count }.from(4).to(2)
- expect(vulnerability_findings.pluck(:id)).to eq([second_duplicate.id, unrelated_finding.id])
+ expect(vulnerability_findings.pluck(:id)).to match_array([third_finding_duplicate.id, unrelated_finding.id])
+ end
+
+ it "removes vulnerabilites without findings" do
+ expect(vulnerabilities.count).to eq(3)
+
+ expect { subject }.to change { vulnerabilities.count }.from(3).to(1)
+
+ expect(vulnerabilities.pluck(:id)).to match_array([vulnerability_for_third_duplicate.id])
end
private
@@ -100,11 +134,12 @@ RSpec.describe Gitlab::BackgroundMigration::RemoveDuplicateVulnerabilitiesFindin
# rubocop:disable Metrics/ParameterLists
def create_finding!(
+ id: nil,
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!(
+ params = {
vulnerability_id: vulnerability_id,
project_id: project_id,
name: name,
@@ -118,7 +153,9 @@ RSpec.describe Gitlab::BackgroundMigration::RemoveDuplicateVulnerabilitiesFindin
metadata_version: metadata_version,
raw_metadata: raw_metadata,
uuid: uuid
- )
+ }
+ params[:id] = id unless id.nil?
+ vulnerability_findings.create!(params)
end
# rubocop:enable Metrics/ParameterLists
diff --git a/spec/lib/gitlab/background_migration/replace_blocked_by_links_spec.rb b/spec/lib/gitlab/background_migration/replace_blocked_by_links_spec.rb
index 561a602fab9..6cfdbb5a14e 100644
--- a/spec/lib/gitlab/background_migration/replace_blocked_by_links_spec.rb
+++ b/spec/lib/gitlab/background_migration/replace_blocked_by_links_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::ReplaceBlockedByLinks, schema: 20201015073808 do
+RSpec.describe Gitlab::BackgroundMigration::ReplaceBlockedByLinks, schema: 20181228175414 do
let(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab-org') }
let(:project) { table(:projects).create!(namespace_id: namespace.id, name: 'gitlab') }
let(:issue1) { table(:issues).create!(project_id: project.id, title: 'a') }
diff --git a/spec/lib/gitlab/background_migration/reset_shared_runners_for_transferred_projects_spec.rb b/spec/lib/gitlab/background_migration/reset_shared_runners_for_transferred_projects_spec.rb
index 68aa64a1c7d..ef90b5674f0 100644
--- a/spec/lib/gitlab/background_migration/reset_shared_runners_for_transferred_projects_spec.rb
+++ b/spec/lib/gitlab/background_migration/reset_shared_runners_for_transferred_projects_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::ResetSharedRunnersForTransferredProjects, schema: 20201110161542 do
+RSpec.describe Gitlab::BackgroundMigration::ResetSharedRunnersForTransferredProjects, schema: 20181228175414 do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
diff --git a/spec/lib/gitlab/background_migration/set_default_iteration_cadences_spec.rb b/spec/lib/gitlab/background_migration/set_default_iteration_cadences_spec.rb
deleted file mode 100644
index 46c919f0854..00000000000
--- a/spec/lib/gitlab/background_migration/set_default_iteration_cadences_spec.rb
+++ /dev/null
@@ -1,80 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::BackgroundMigration::SetDefaultIterationCadences, schema: 20201231133921 do
- let(:namespaces) { table(:namespaces) }
- let(:iterations) { table(:sprints) }
- let(:iterations_cadences) { table(:iterations_cadences) }
-
- describe '#perform' do
- context 'when no iteration cadences exists' do
- let!(:group_1) { namespaces.create!(name: 'group 1', path: 'group-1') }
- let!(:group_2) { namespaces.create!(name: 'group 2', path: 'group-2') }
- let!(:group_3) { namespaces.create!(name: 'group 3', path: 'group-3') }
-
- let!(:iteration_1) { iterations.create!(group_id: group_1.id, iid: 1, title: 'Iteration 1', start_date: 10.days.ago, due_date: 8.days.ago) }
- let!(:iteration_2) { iterations.create!(group_id: group_3.id, iid: 1, title: 'Iteration 2', start_date: 10.days.ago, due_date: 8.days.ago) }
- let!(:iteration_3) { iterations.create!(group_id: group_3.id, iid: 1, title: 'Iteration 3', start_date: 5.days.ago, due_date: 2.days.ago) }
-
- subject { described_class.new.perform(group_1.id, group_2.id, group_3.id, namespaces.last.id + 1) }
-
- before do
- subject
- end
-
- it 'creates iterations_cadence records for the requested groups' do
- expect(iterations_cadences.count).to eq(2)
- end
-
- it 'assigns the iteration cadences to the iterations correctly' do
- iterations_cadence = iterations_cadences.find_by(group_id: group_1.id)
- iteration_records = iterations.where(iterations_cadence_id: iterations_cadence.id)
-
- expect(iterations_cadence.start_date).to eq(iteration_1.start_date)
- expect(iterations_cadence.last_run_date).to eq(iteration_1.start_date)
- expect(iterations_cadence.title).to eq('group 1 Iterations')
- expect(iteration_records.size).to eq(1)
- expect(iteration_records.first.id).to eq(iteration_1.id)
-
- iterations_cadence = iterations_cadences.find_by(group_id: group_3.id)
- iteration_records = iterations.where(iterations_cadence_id: iterations_cadence.id)
-
- expect(iterations_cadence.start_date).to eq(iteration_3.start_date)
- expect(iterations_cadence.last_run_date).to eq(iteration_3.start_date)
- expect(iterations_cadence.title).to eq('group 3 Iterations')
- expect(iteration_records.size).to eq(2)
- expect(iteration_records.first.id).to eq(iteration_2.id)
- expect(iteration_records.second.id).to eq(iteration_3.id)
- end
-
- it 'does not call Group class' do
- expect(::Group).not_to receive(:where)
-
- subject
- end
- end
-
- context 'when an iteration cadence exists for a group' do
- let!(:group) { namespaces.create!(name: 'group', path: 'group') }
-
- let!(:iterations_cadence_1) { iterations_cadences.create!(group_id: group.id, start_date: 2.days.ago, title: 'Cadence 1') }
-
- let!(:iteration_1) { iterations.create!(group_id: group.id, iid: 1, title: 'Iteration 1', start_date: 10.days.ago, due_date: 8.days.ago) }
- let!(:iteration_2) { iterations.create!(group_id: group.id, iterations_cadence_id: iterations_cadence_1.id, iid: 2, title: 'Iteration 2', start_date: 5.days.ago, due_date: 3.days.ago) }
-
- subject { described_class.new.perform(group.id) }
-
- it 'does not create a new iterations_cadence' do
- expect { subject }.not_to change { iterations_cadences.count }
- end
-
- it 'assigns iteration cadences to iterations if needed' do
- subject
-
- expect(iteration_1.reload.iterations_cadence_id).to eq(iterations_cadence_1.id)
- expect(iteration_2.reload.iterations_cadence_id).to eq(iterations_cadence_1.id)
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/background_migration/set_merge_request_diff_files_count_spec.rb b/spec/lib/gitlab/background_migration/set_merge_request_diff_files_count_spec.rb
index f23518625e4..1fdbdf25706 100644
--- a/spec/lib/gitlab/background_migration/set_merge_request_diff_files_count_spec.rb
+++ b/spec/lib/gitlab/background_migration/set_merge_request_diff_files_count_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::SetMergeRequestDiffFilesCount, schema: 20200807152315 do
+RSpec.describe Gitlab::BackgroundMigration::SetMergeRequestDiffFilesCount, schema: 20181228175414 do
let(:merge_request_diff_files) { table(:merge_request_diff_files) }
let(:merge_request_diffs) { table(:merge_request_diffs) }
let(:merge_requests) { table(:merge_requests) }
diff --git a/spec/lib/gitlab/background_migration/set_null_external_diff_store_to_local_value_spec.rb b/spec/lib/gitlab/background_migration/set_null_external_diff_store_to_local_value_spec.rb
deleted file mode 100644
index 6079ad2dd2a..00000000000
--- a/spec/lib/gitlab/background_migration/set_null_external_diff_store_to_local_value_spec.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-# The test setup must begin before
-# 20200804041930_add_not_null_constraint_on_external_diff_store_to_merge_request_diffs.rb
-# has run, or else we cannot insert a row with `NULL` `external_diff_store` to
-# test against.
-RSpec.describe Gitlab::BackgroundMigration::SetNullExternalDiffStoreToLocalValue, schema: 20200804035230 do
- let!(:merge_request_diffs) { table(:merge_request_diffs) }
- let!(:merge_requests) { table(:merge_requests) }
- let!(:namespaces) { table(:namespaces) }
- let!(:projects) { table(:projects) }
- let!(:namespace) { namespaces.create!(name: 'foo', path: 'foo') }
- let!(:project) { projects.create!(namespace_id: namespace.id) }
- let!(:merge_request) { merge_requests.create!(source_branch: 'x', target_branch: 'master', target_project_id: project.id) }
-
- it 'correctly migrates nil external_diff_store to 1' do
- external_diff_store_1 = merge_request_diffs.create!(external_diff_store: 1, merge_request_id: merge_request.id)
- external_diff_store_2 = merge_request_diffs.create!(external_diff_store: 2, merge_request_id: merge_request.id)
- external_diff_store_nil = merge_request_diffs.create!(external_diff_store: nil, merge_request_id: merge_request.id)
-
- described_class.new.perform(external_diff_store_1.id, external_diff_store_nil.id)
-
- external_diff_store_1.reload
- external_diff_store_2.reload
- external_diff_store_nil.reload
-
- expect(external_diff_store_1.external_diff_store).to eq(1) # unchanged
- expect(external_diff_store_2.external_diff_store).to eq(2) # unchanged
- expect(external_diff_store_nil.external_diff_store).to eq(1) # nil => 1
- end
-end
diff --git a/spec/lib/gitlab/background_migration/set_null_package_files_file_store_to_local_value_spec.rb b/spec/lib/gitlab/background_migration/set_null_package_files_file_store_to_local_value_spec.rb
deleted file mode 100644
index 40d41262fc7..00000000000
--- a/spec/lib/gitlab/background_migration/set_null_package_files_file_store_to_local_value_spec.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-# The test setup must begin before
-# 20200806004742_add_not_null_constraint_on_file_store_to_package_files.rb
-# has run, or else we cannot insert a row with `NULL` `file_store` to
-# test against.
-RSpec.describe Gitlab::BackgroundMigration::SetNullPackageFilesFileStoreToLocalValue, schema: 20200806004232 do
- let!(:packages_package_files) { table(:packages_package_files) }
- let!(:packages_packages) { table(:packages_packages) }
- let!(:projects) { table(:projects) }
- let!(:namespaces) { table(:namespaces) }
- let!(:namespace) { namespaces.create!(name: 'foo', path: 'foo') }
- let!(:project) { projects.create!(namespace_id: namespace.id) }
- let!(:package) { packages_packages.create!(project_id: project.id, name: 'bar', package_type: 1) }
-
- it 'correctly migrates nil file_store to 1' do
- file_store_1 = packages_package_files.create!(file_store: 1, file_name: 'foo_1', file: 'foo_1', package_id: package.id)
- file_store_2 = packages_package_files.create!(file_store: 2, file_name: 'foo_2', file: 'foo_2', package_id: package.id)
- file_store_nil = packages_package_files.create!(file_store: nil, file_name: 'foo_nil', file: 'foo_nil', package_id: package.id)
-
- described_class.new.perform(file_store_1.id, file_store_nil.id)
-
- file_store_1.reload
- file_store_2.reload
- file_store_nil.reload
-
- expect(file_store_1.file_store).to eq(1) # unchanged
- expect(file_store_2.file_store).to eq(2) # unchanged
- expect(file_store_nil.file_store).to eq(1) # nil => 1
- end
-end
diff --git a/spec/lib/gitlab/background_migration/steal_migrate_merge_request_diff_commit_users_spec.rb b/spec/lib/gitlab/background_migration/steal_migrate_merge_request_diff_commit_users_spec.rb
index f2fb2ab6b6e..841a7f306d7 100644
--- a/spec/lib/gitlab/background_migration/steal_migrate_merge_request_diff_commit_users_spec.rb
+++ b/spec/lib/gitlab/background_migration/steal_migrate_merge_request_diff_commit_users_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::StealMigrateMergeRequestDiffCommitUsers do
+RSpec.describe Gitlab::BackgroundMigration::StealMigrateMergeRequestDiffCommitUsers, schema: 20211012134316 do
let(:migration) { described_class.new }
describe '#perform' do
diff --git a/spec/lib/gitlab/background_migration/update_existing_subgroup_to_match_visibility_level_of_parent_spec.rb b/spec/lib/gitlab/background_migration/update_existing_subgroup_to_match_visibility_level_of_parent_spec.rb
index 6c0a1d3a5b0..de9799c3642 100644
--- a/spec/lib/gitlab/background_migration/update_existing_subgroup_to_match_visibility_level_of_parent_spec.rb
+++ b/spec/lib/gitlab/background_migration/update_existing_subgroup_to_match_visibility_level_of_parent_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::UpdateExistingSubgroupToMatchVisibilityLevelOfParent, schema: 2020_01_10_121314 do
+RSpec.describe Gitlab::BackgroundMigration::UpdateExistingSubgroupToMatchVisibilityLevelOfParent, schema: 20181228175414 do
include MigrationHelpers::NamespacesHelpers
context 'private visibility level' do
diff --git a/spec/lib/gitlab/background_migration/update_existing_users_that_require_two_factor_auth_spec.rb b/spec/lib/gitlab/background_migration/update_existing_users_that_require_two_factor_auth_spec.rb
index bebb398413b..33f5e38100e 100644
--- a/spec/lib/gitlab/background_migration/update_existing_users_that_require_two_factor_auth_spec.rb
+++ b/spec/lib/gitlab/background_migration/update_existing_users_that_require_two_factor_auth_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::UpdateExistingUsersThatRequireTwoFactorAuth, schema: 20201030121314 do
+RSpec.describe Gitlab::BackgroundMigration::UpdateExistingUsersThatRequireTwoFactorAuth, schema: 20181228175414 do
include MigrationHelpers::NamespacesHelpers
let(:group_with_2fa_parent) { create_namespace('parent', Gitlab::VisibilityLevel::PRIVATE) }
diff --git a/spec/lib/gitlab/background_migration/user_mentions/create_resource_user_mention_spec.rb b/spec/lib/gitlab/background_migration/user_mentions/create_resource_user_mention_spec.rb
index 2dae4a65eeb..7af11ffa1e0 100644
--- a/spec/lib/gitlab/background_migration/user_mentions/create_resource_user_mention_spec.rb
+++ b/spec/lib/gitlab/background_migration/user_mentions/create_resource_user_mention_spec.rb
@@ -1,120 +1,8 @@
# frozen_string_literal: true
require 'spec_helper'
-require './db/post_migrate/20200128134110_migrate_commit_notes_mentions_to_db'
-require './db/post_migrate/20200211155539_migrate_merge_request_mentions_to_db'
-
-RSpec.describe Gitlab::BackgroundMigration::UserMentions::CreateResourceUserMention, schema: 20200211155539 do
- include MigrationsHelpers
-
- context 'when migrating data' do
- let(:users) { table(:users) }
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:notes) { table(:notes) }
- let(:routes) { table(:routes) }
-
- let(:author) { users.create!(email: 'author@example.com', notification_email: 'author@example.com', name: 'author', username: 'author', projects_limit: 10, state: 'active') }
- let(:member) { users.create!(email: 'member@example.com', notification_email: 'member@example.com', name: 'member', username: 'member', projects_limit: 10, state: 'active') }
- let(:admin) { users.create!(email: 'administrator@example.com', notification_email: 'administrator@example.com', name: 'administrator', username: 'administrator', admin: 1, projects_limit: 10, state: 'active') }
- let(:john_doe) { users.create!(email: 'john_doe@example.com', notification_email: 'john_doe@example.com', name: 'john_doe', username: 'john_doe', projects_limit: 10, state: 'active') }
- let(:skipped) { users.create!(email: 'skipped@example.com', notification_email: 'skipped@example.com', name: 'skipped', username: 'skipped', projects_limit: 10, state: 'active') }
-
- let(:mentioned_users) { [author, member, admin, john_doe, skipped] }
- let(:mentioned_users_refs) { mentioned_users.map { |u| "@#{u.username}" }.join(' ') }
-
- let(:group) { namespaces.create!(name: 'test1', path: 'test1', runners_token: 'my-token1', project_creation_level: 1, visibility_level: 20, type: 'Group') }
- let(:inaccessible_group) { namespaces.create!(name: 'test2', path: 'test2', runners_token: 'my-token2', project_creation_level: 1, visibility_level: 0, type: 'Group') }
- let(:project) { projects.create!(name: 'gitlab1', path: 'gitlab1', namespace_id: group.id, visibility_level: 0) }
-
- let(:mentioned_groups) { [group, inaccessible_group] }
- let(:group_mentions) { [group, inaccessible_group].map { |gr| "@#{gr.path}" }.join(' ') }
- let(:description_mentions) { "description with mentions #{mentioned_users_refs} and #{group_mentions}" }
-
- before do
- # build personal namespaces and routes for users
- mentioned_users.each do |u|
- namespace = namespaces.create!(path: u.username, name: u.name, runners_token: "my-token-u#{u.id}", owner_id: u.id, type: nil)
- routes.create!(path: namespace.path, source_type: 'Namespace', source_id: namespace.id)
- end
-
- # build namespaces and routes for groups
- mentioned_groups.each do |gr|
- routes.create!(path: gr.path, source_type: 'Namespace', source_id: gr.id)
- end
- end
-
- context 'migrate merge request mentions' do
- let(:merge_requests) { table(:merge_requests) }
- let(:merge_request_user_mentions) { table(:merge_request_user_mentions) }
-
- let!(:mr1) do
- merge_requests.create!(
- title: "title 1", state_id: 1, target_branch: 'feature1', source_branch: 'master',
- source_project_id: project.id, target_project_id: project.id, author_id: author.id,
- description: description_mentions
- )
- end
-
- let!(:mr2) do
- merge_requests.create!(
- title: "title 2", state_id: 1, target_branch: 'feature2', source_branch: 'master',
- source_project_id: project.id, target_project_id: project.id, author_id: author.id,
- description: 'some description'
- )
- end
-
- let!(:mr3) do
- merge_requests.create!(
- title: "title 3", state_id: 1, target_branch: 'feature3', source_branch: 'master',
- source_project_id: project.id, target_project_id: project.id, author_id: author.id,
- description: 'description with an email@example.com and some other @ char here.')
- end
-
- let(:user_mentions) { merge_request_user_mentions }
- let(:resource) { merge_request }
-
- it_behaves_like 'resource mentions migration', MigrateMergeRequestMentionsToDb, 'MergeRequest'
-
- context 'when FF disabled' do
- before do
- stub_feature_flags(migrate_user_mentions: false)
- end
-
- it_behaves_like 'resource migration not run', MigrateMergeRequestMentionsToDb, 'MergeRequest'
- end
- end
-
- context 'migrate commit mentions' do
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') }
- let(:commit) { Commit.new(RepoHelpers.sample_commit, project) }
- let(:commit_user_mentions) { table(:commit_user_mentions) }
-
- let!(:note1) { notes.create!(commit_id: commit.id, noteable_type: 'Commit', project_id: project.id, author_id: author.id, note: description_mentions) }
- let!(:note2) { notes.create!(commit_id: commit.id, noteable_type: 'Commit', project_id: project.id, author_id: author.id, note: 'sample note') }
- let!(:note3) { notes.create!(commit_id: commit.id, noteable_type: 'Commit', project_id: project.id, author_id: author.id, note: description_mentions, system: true) }
-
- # this not does not have actual mentions
- let!(:note4) { notes.create!(commit_id: commit.id, noteable_type: 'Commit', project_id: project.id, author_id: author.id, note: 'note for an email@somesite.com and some other random @ ref' ) }
- # this should have pointed to an innexisted commit record in a commits table
- # but because commit is not an AR we'll just make it so that it does not have mentions
- let!(:note5) { notes.create!(commit_id: 'abc', noteable_type: 'Commit', project_id: project.id, author_id: author.id, note: 'note for an email@somesite.com and some other random @ ref') }
-
- let(:user_mentions) { commit_user_mentions }
- let(:resource) { commit }
-
- it_behaves_like 'resource notes mentions migration', MigrateCommitNotesMentionsToDb, 'Commit'
-
- context 'when FF disabled' do
- before do
- stub_feature_flags(migrate_user_mentions: false)
- end
-
- it_behaves_like 'resource notes migration not run', MigrateCommitNotesMentionsToDb, 'Commit'
- end
- end
- end
+RSpec.describe Gitlab::BackgroundMigration::UserMentions::CreateResourceUserMention, schema: 20181228175414 do
context 'checks no_quote_columns' do
it 'has correct no_quote_columns' do
expect(Gitlab::BackgroundMigration::UserMentions::Models::MergeRequest.no_quote_columns).to match([:note_id, :merge_request_id])
diff --git a/spec/lib/gitlab/background_migration/wrongfully_confirmed_email_unconfirmer_spec.rb b/spec/lib/gitlab/background_migration/wrongfully_confirmed_email_unconfirmer_spec.rb
index 07f4429f7d9..5c197526a55 100644
--- a/spec/lib/gitlab/background_migration/wrongfully_confirmed_email_unconfirmer_spec.rb
+++ b/spec/lib/gitlab/background_migration/wrongfully_confirmed_email_unconfirmer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::WrongfullyConfirmedEmailUnconfirmer, schema: 20200615111857 do
+RSpec.describe Gitlab::BackgroundMigration::WrongfullyConfirmedEmailUnconfirmer, schema: 20181228175414 do
let(:users) { table(:users) }
let(:emails) { table(:emails) }
let(:user_synced_attributes_metadata) { table(:user_synced_attributes_metadata) }
diff --git a/spec/lib/gitlab/background_migration_spec.rb b/spec/lib/gitlab/background_migration_spec.rb
index f32e6891716..777dc8112a7 100644
--- a/spec/lib/gitlab/background_migration_spec.rb
+++ b/spec/lib/gitlab/background_migration_spec.rb
@@ -3,6 +3,14 @@
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration do
+ let(:coordinator) { described_class::JobCoordinator.for_database(:main) }
+
+ before do
+ allow(described_class).to receive(:coordinator_for_database)
+ .with(:main)
+ .and_return(coordinator)
+ end
+
describe '.queue' do
it 'returns background migration worker queue' do
expect(described_class.queue)
@@ -11,7 +19,7 @@ RSpec.describe Gitlab::BackgroundMigration do
end
describe '.steal' do
- context 'when there are enqueued jobs present' do
+ context 'when the queue contains unprocessed jobs' do
let(:queue) do
[
double(args: ['Foo', [10, 20]], klass: 'BackgroundMigrationWorker'),
@@ -22,110 +30,34 @@ RSpec.describe Gitlab::BackgroundMigration do
before do
allow(Sidekiq::Queue).to receive(:new)
- .with(described_class.queue)
+ .with(coordinator.queue)
.and_return(queue)
end
- context 'when queue contains unprocessed jobs' do
- it 'steals jobs from a queue' do
- expect(queue[0]).to receive(:delete).and_return(true)
-
- expect(described_class).to receive(:perform)
- .with('Foo', [10, 20])
-
- described_class.steal('Foo')
- end
-
- it 'does not steal job that has already been taken' do
- expect(queue[0]).to receive(:delete).and_return(false)
-
- expect(described_class).not_to receive(:perform)
-
- described_class.steal('Foo')
- end
-
- it 'does not steal jobs for a different migration' do
- expect(described_class).not_to receive(:perform)
+ it 'uses the coordinator to steal jobs' do
+ expect(queue[0]).to receive(:delete).and_return(true)
- expect(queue[0]).not_to receive(:delete)
-
- described_class.steal('Baz')
- end
-
- context 'when a custom predicate is given' do
- it 'steals jobs that match the predicate' do
- expect(queue[0]).to receive(:delete).and_return(true)
-
- expect(described_class).to receive(:perform)
- .with('Foo', [10, 20])
-
- described_class.steal('Foo') { |job| job.args.second.first == 10 && job.args.second.second == 20 }
- end
+ expect(coordinator).to receive(:steal).with('Foo', retry_dead_jobs: false).and_call_original
+ expect(coordinator).to receive(:perform).with('Foo', [10, 20])
- it 'does not steal jobs that do not match the predicate' do
- expect(described_class).not_to receive(:perform)
-
- expect(queue[0]).not_to receive(:delete)
-
- described_class.steal('Foo') { |(arg1, _)| arg1 == 5 }
- end
- end
+ described_class.steal('Foo')
end
- context 'when one of the jobs raises an error' do
- let(:migration) { spy(:migration) }
-
- let(:queue) do
- [double(args: ['Foo', [10, 20]], klass: 'BackgroundMigrationWorker'),
- double(args: ['Foo', [20, 30]], klass: 'BackgroundMigrationWorker')]
- end
-
- before do
- stub_const("#{described_class}::Foo", migration)
-
- allow(queue[0]).to receive(:delete).and_return(true)
- allow(queue[1]).to receive(:delete).and_return(true)
- end
-
- it 'enqueues the migration again and re-raises the error' do
- allow(migration).to receive(:perform).with(10, 20)
- .and_raise(Exception, 'Migration error').once
+ context 'when a custom predicate is given' do
+ it 'steals jobs that match the predicate' do
+ expect(queue[0]).to receive(:delete).and_return(true)
- expect(BackgroundMigrationWorker).to receive(:perform_async)
- .with('Foo', [10, 20]).once
+ expect(coordinator).to receive(:perform).with('Foo', [10, 20])
- expect { described_class.steal('Foo') }.to raise_error(Exception)
+ described_class.steal('Foo') { |job| job.args.second.first == 10 && job.args.second.second == 20 }
end
- end
- end
- context 'when there are scheduled jobs present', :redis do
- it 'steals all jobs from the scheduled sets' do
- Sidekiq::Testing.disable! do
- BackgroundMigrationWorker.perform_in(10.minutes, 'Object')
-
- expect(Sidekiq::ScheduledSet.new).to be_one
- expect(described_class).to receive(:perform).with('Object', any_args)
-
- described_class.steal('Object')
+ it 'does not steal jobs that do not match the predicate' do
+ expect(coordinator).not_to receive(:perform)
- expect(Sidekiq::ScheduledSet.new).to be_none
- end
- end
- end
-
- context 'when there are enqueued and scheduled jobs present', :redis do
- it 'steals from the scheduled sets queue first' do
- Sidekiq::Testing.disable! do
- expect(described_class).to receive(:perform)
- .with('Object', [1]).ordered
- expect(described_class).to receive(:perform)
- .with('Object', [2]).ordered
-
- BackgroundMigrationWorker.perform_async('Object', [2])
- BackgroundMigrationWorker.perform_in(10.minutes, 'Object', [1])
+ expect(queue[0]).not_to receive(:delete)
- described_class.steal('Object')
+ described_class.steal('Foo') { |(arg1, _)| arg1 == 5 }
end
end
end
@@ -146,14 +78,10 @@ RSpec.describe Gitlab::BackgroundMigration do
it 'steals from the dead and retry queue' do
Sidekiq::Testing.disable! do
- expect(described_class).to receive(:perform)
- .with('Object', [1]).ordered
- expect(described_class).to receive(:perform)
- .with('Object', [2]).ordered
- expect(described_class).to receive(:perform)
- .with('Object', [3]).ordered
- expect(described_class).to receive(:perform)
- .with('Object', [4]).ordered
+ expect(coordinator).to receive(:perform).with('Object', [1]).ordered
+ expect(coordinator).to receive(:perform).with('Object', [2]).ordered
+ expect(coordinator).to receive(:perform).with('Object', [3]).ordered
+ expect(coordinator).to receive(:perform).with('Object', [4]).ordered
BackgroundMigrationWorker.perform_async('Object', [2])
BackgroundMigrationWorker.perform_in(10.minutes, 'Object', [1])
@@ -171,131 +99,54 @@ RSpec.describe Gitlab::BackgroundMigration do
stub_const("#{described_class.name}::Foo", migration)
end
- it 'performs a background migration' do
+ it 'uses the coordinator to perform a background migration' do
+ expect(coordinator).to receive(:perform).with('Foo', [10, 20]).and_call_original
expect(migration).to receive(:perform).with(10, 20).once
described_class.perform('Foo', [10, 20])
end
+ end
- context 'backward compatibility' do
- it 'performs a background migration for fully-qualified job classes' do
- expect(migration).to receive(:perform).with(10, 20).once
- expect(Gitlab::ErrorTracking)
- .to receive(:track_and_raise_for_dev_exception)
- .with(instance_of(StandardError), hash_including(:class_name))
-
- described_class.perform('Gitlab::BackgroundMigration::Foo', [10, 20])
+ describe '.exists?', :redis do
+ before do
+ Sidekiq::Testing.disable! do
+ MergeWorker.perform_async('Bar')
+ BackgroundMigrationWorker.perform_async('Foo')
end
end
- end
- describe '.remaining', :redis do
- context 'when there are jobs remaining' do
- before do
- Sidekiq::Testing.disable! do
- MergeWorker.perform_async('Foo')
- MergeWorker.perform_in(10.minutes, 'Foo')
-
- 5.times do
- BackgroundMigrationWorker.perform_async('Foo')
- end
- 3.times do
- BackgroundMigrationWorker.perform_in(10.minutes, 'Foo')
- end
- end
- end
+ it 'uses the coordinator to find if a job exists' do
+ expect(coordinator).to receive(:exists?).with('Foo', []).and_call_original
- it 'returns the enqueued jobs plus the scheduled jobs' do
- expect(described_class.remaining).to eq(8)
- end
+ expect(described_class.exists?('Foo')).to eq(true)
end
- context 'when there are no jobs remaining' do
- it 'returns zero' do
- expect(described_class.remaining).to be_zero
- end
+ it 'uses the coordinator to find a job does not exist' do
+ expect(coordinator).to receive(:exists?).with('Bar', []).and_call_original
+
+ expect(described_class.exists?('Bar')).to eq(false)
end
end
- describe '.exists?', :redis do
- context 'when there are enqueued jobs present' do
- before do
- Sidekiq::Testing.disable! do
- MergeWorker.perform_async('Bar')
+ describe '.remaining', :redis do
+ before do
+ Sidekiq::Testing.disable! do
+ MergeWorker.perform_async('Foo')
+ MergeWorker.perform_in(10.minutes, 'Foo')
+
+ 5.times do
BackgroundMigrationWorker.perform_async('Foo')
end
- end
-
- it 'returns true if specific job exists' do
- expect(described_class.exists?('Foo')).to eq(true)
- end
-
- it 'returns false if specific job does not exist' do
- expect(described_class.exists?('Bar')).to eq(false)
- end
- end
-
- context 'when there are scheduled jobs present' do
- before do
- Sidekiq::Testing.disable! do
- MergeWorker.perform_in(10.minutes, 'Bar')
+ 3.times do
BackgroundMigrationWorker.perform_in(10.minutes, 'Foo')
end
end
-
- it 'returns true if specific job exists' do
- expect(described_class.exists?('Foo')).to eq(true)
- end
-
- it 'returns false if specific job does not exist' do
- expect(described_class.exists?('Bar')).to eq(false)
- end
- end
- end
-
- describe '.dead_jobs?' do
- let(:queue) do
- [
- double(args: ['Foo', [10, 20]], klass: 'BackgroundMigrationWorker'),
- double(args: ['Bar'], klass: 'MergeWorker')
- ]
end
- context 'when there are dead jobs present' do
- before do
- allow(Sidekiq::DeadSet).to receive(:new).and_return(queue)
- end
-
- it 'returns true if specific job exists' do
- expect(described_class.dead_jobs?('Foo')).to eq(true)
- end
+ it 'uses the coordinator to find the number of remaining jobs' do
+ expect(coordinator).to receive(:remaining).and_call_original
- it 'returns false if specific job does not exist' do
- expect(described_class.dead_jobs?('Bar')).to eq(false)
- end
- end
- end
-
- describe '.retrying_jobs?' do
- let(:queue) do
- [
- double(args: ['Foo', [10, 20]], klass: 'BackgroundMigrationWorker'),
- double(args: ['Bar'], klass: 'MergeWorker')
- ]
- end
-
- context 'when there are dead jobs present' do
- before do
- allow(Sidekiq::RetrySet).to receive(:new).and_return(queue)
- end
-
- it 'returns true if specific job exists' do
- expect(described_class.retrying_jobs?('Foo')).to eq(true)
- end
-
- it 'returns false if specific job does not exist' do
- expect(described_class.retrying_jobs?('Bar')).to eq(false)
- end
+ expect(described_class.remaining).to eq(8)
end
end
end
diff --git a/spec/lib/gitlab/bare_repository_import/importer_spec.rb b/spec/lib/gitlab/bare_repository_import/importer_spec.rb
index e09430a858c..b0d721a74ce 100644
--- a/spec/lib/gitlab/bare_repository_import/importer_spec.rb
+++ b/spec/lib/gitlab/bare_repository_import/importer_spec.rb
@@ -89,10 +89,8 @@ RSpec.describe Gitlab::BareRepositoryImport::Importer, :seed_helper do
project = Project.find_by_full_path(project_path)
repo_path = "#{project.disk_path}.git"
- hook_path = File.join(repo_path, 'hooks')
expect(gitlab_shell.repository_exists?(project.repository_storage, repo_path)).to be(true)
- expect(TestEnv.storage_dir_exists?(project.repository_storage, hook_path)).to be(true)
end
context 'hashed storage enabled' do
diff --git a/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb
index 4e4d921d67f..f9313f0ff28 100644
--- a/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb
@@ -142,7 +142,7 @@ RSpec.describe Gitlab::BitbucketServerImport::Importer do
expect { subject.execute }.to change { MergeRequest.count }.by(1)
merge_request = MergeRequest.first
- expect(merge_request.author).to eq(pull_request_author)
+ expect(merge_request.author).to eq(expected_author)
end
end
@@ -151,7 +151,25 @@ RSpec.describe Gitlab::BitbucketServerImport::Importer do
stub_feature_flags(bitbucket_server_user_mapping_by_username: false)
end
- include_examples 'imports pull requests'
+ context 'when email is not present' do
+ before do
+ allow(pull_request).to receive(:author_email).and_return(nil)
+ end
+
+ let(:expected_author) { project_creator }
+
+ include_examples 'imports pull requests'
+ end
+
+ context 'when email is present' do
+ before do
+ allow(pull_request).to receive(:author_email).and_return(pull_request_author.email)
+ end
+
+ let(:expected_author) { pull_request_author }
+
+ include_examples 'imports pull requests'
+ end
end
context 'when bitbucket_server_user_mapping_by_username feature flag is enabled' do
@@ -159,19 +177,24 @@ RSpec.describe Gitlab::BitbucketServerImport::Importer do
stub_feature_flags(bitbucket_server_user_mapping_by_username: true)
end
- include_examples 'imports pull requests' do
- context 'when username is not present' do
- before do
- allow(pull_request).to receive(:author_username).and_return(nil)
- end
+ context 'when username is not present' do
+ before do
+ allow(pull_request).to receive(:author_username).and_return(nil)
+ end
- it 'maps by email' do
- expect { subject.execute }.to change { MergeRequest.count }.by(1)
+ let(:expected_author) { project_creator }
- merge_request = MergeRequest.first
- expect(merge_request.author).to eq(pull_request_author)
- end
+ include_examples 'imports pull requests'
+ end
+
+ context 'when username is present' do
+ before do
+ allow(pull_request).to receive(:author_username).and_return(pull_request_author.username)
end
+
+ let(:expected_author) { pull_request_author }
+
+ include_examples 'imports pull requests'
end
end
@@ -228,7 +251,23 @@ RSpec.describe Gitlab::BitbucketServerImport::Importer do
allow(subject.client).to receive(:activities).and_return([pr_comment])
end
- it 'maps by email' do
+ it 'defaults to import user' do
+ expect { subject.execute }.to change { MergeRequest.count }.by(1)
+
+ merge_request = MergeRequest.first
+ expect(merge_request.notes.count).to eq(1)
+ note = merge_request.notes.first
+ expect(note.author).to eq(project_creator)
+ end
+ end
+
+ context 'when username is present' do
+ before do
+ allow(pr_note).to receive(:author_username).and_return(note_author.username)
+ allow(subject.client).to receive(:activities).and_return([pr_comment])
+ end
+
+ it 'maps by username' do
expect { subject.execute }.to change { MergeRequest.count }.by(1)
merge_request = MergeRequest.first
@@ -241,7 +280,7 @@ RSpec.describe Gitlab::BitbucketServerImport::Importer do
end
context 'metrics' do
- let(:histogram) { double(:histogram) }
+ let(:histogram) { double(:histogram).as_null_object }
let(:counter) { double('counter', increment: true) }
before do
@@ -276,7 +315,6 @@ RSpec.describe Gitlab::BitbucketServerImport::Importer do
)
expect(counter).to receive(:increment)
- allow(histogram).to receive(:observe).with({ importer: :bitbucket_server_importer }, anything)
subject.execute
end
@@ -384,13 +422,13 @@ RSpec.describe Gitlab::BitbucketServerImport::Importer do
allow(inline_note).to receive(:author_username).and_return(nil)
end
- it 'maps by email' do
+ it 'defaults to import user' do
expect { subject.execute }.to change { MergeRequest.count }.by(1)
notes = MergeRequest.first.notes.order(:id).to_a
- expect(notes.first.author).to eq(inline_note_author)
- expect(notes.last.author).to eq(reply_author)
+ expect(notes.first.author).to eq(project_creator)
+ expect(notes.last.author).to eq(project_creator)
end
end
end
diff --git a/spec/lib/gitlab/blob_helper_spec.rb b/spec/lib/gitlab/blob_helper_spec.rb
index 65fa5bf0120..a2f20dcd4fc 100644
--- a/spec/lib/gitlab/blob_helper_spec.rb
+++ b/spec/lib/gitlab/blob_helper_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe Gitlab::BlobHelper do
let(:project) { create(:project) }
let(:blob) { fake_blob(path: 'file.txt') }
+ let(:webp_blob) { fake_blob(path: 'file.webp') }
let(:large_blob) { fake_blob(path: 'test.pdf', size: 2.megabytes, binary: true) }
describe '#extname' do
@@ -62,8 +63,15 @@ RSpec.describe Gitlab::BlobHelper do
end
describe '#image?' do
- it 'returns false' do
- expect(blob.image?).to be_falsey
+ context 'with a .txt file' do
+ it 'returns false' do
+ expect(blob.image?).to be_falsey
+ end
+ end
+ context 'with a .webp file' do
+ it 'returns true' do
+ expect(webp_blob.image?).to be_truthy
+ end
end
end
diff --git a/spec/lib/gitlab/ci/artifact_file_reader_spec.rb b/spec/lib/gitlab/ci/artifact_file_reader_spec.rb
index 83a37655ea9..e982f0eb015 100644
--- a/spec/lib/gitlab/ci/artifact_file_reader_spec.rb
+++ b/spec/lib/gitlab/ci/artifact_file_reader_spec.rb
@@ -18,17 +18,6 @@ RSpec.describe Gitlab::Ci::ArtifactFileReader do
expect(YAML.safe_load(subject).keys).to contain_exactly('rspec', 'time', 'custom')
end
- context 'when FF ci_new_artifact_file_reader is disabled' do
- before do
- stub_feature_flags(ci_new_artifact_file_reader: false)
- end
-
- it 'returns the content at the path' do
- is_expected.to be_present
- expect(YAML.safe_load(subject).keys).to contain_exactly('rspec', 'time', 'custom')
- end
- end
-
context 'when path does not exist' do
let(:path) { 'file/does/not/exist.txt' }
let(:expected_error) do
diff --git a/spec/lib/gitlab/ci/artifacts/metrics_spec.rb b/spec/lib/gitlab/ci/artifacts/metrics_spec.rb
index 3a2095498ec..0ce76285b03 100644
--- a/spec/lib/gitlab/ci/artifacts/metrics_spec.rb
+++ b/spec/lib/gitlab/ci/artifacts/metrics_spec.rb
@@ -10,9 +10,9 @@ RSpec.describe Gitlab::Ci::Artifacts::Metrics, :prometheus do
let(:counter) { metrics.send(:destroyed_artifacts_counter) }
it 'increments a single counter' do
- subject.increment_destroyed_artifacts(10)
- subject.increment_destroyed_artifacts(20)
- subject.increment_destroyed_artifacts(30)
+ subject.increment_destroyed_artifacts_count(10)
+ subject.increment_destroyed_artifacts_count(20)
+ subject.increment_destroyed_artifacts_count(30)
expect(counter.get).to eq 60
expect(counter.values.count).to eq 1
diff --git a/spec/lib/gitlab/ci/build/auto_retry_spec.rb b/spec/lib/gitlab/ci/build/auto_retry_spec.rb
index fc5999d59ac..9ff9200322e 100644
--- a/spec/lib/gitlab/ci/build/auto_retry_spec.rb
+++ b/spec/lib/gitlab/ci/build/auto_retry_spec.rb
@@ -25,6 +25,8 @@ RSpec.describe Gitlab::Ci::Build::AutoRetry do
"quota is exceeded" | 0 | { max: 2 } | :ci_quota_exceeded | false
"no matching runner" | 0 | { max: 2 } | :no_matching_runner | false
"missing dependencies" | 0 | { max: 2 } | :missing_dependency_failure | false
+ "forward deployment failure" | 0 | { max: 2 } | :forward_deployment_failure | false
+ "environment creation failure" | 0 | { max: 2 } | :environment_creation_failure | false
end
with_them do
diff --git a/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb b/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb
index 86dd5569a96..f192862c1c4 100644
--- a/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb
+++ b/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb
@@ -3,10 +3,8 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists do
- describe '#satisfied_by?' do
- let(:pipeline) { build(:ci_pipeline, project: project, sha: project.repository.head_commit.sha) }
-
- subject { described_class.new(globs).satisfied_by?(pipeline, nil) }
+ shared_examples 'an exists rule with a context' do
+ subject { described_class.new(globs).satisfied_by?(pipeline, context) }
it_behaves_like 'a glob matching rule' do
let(:project) { create(:project, :custom_repo, files: files) }
@@ -24,4 +22,26 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists do
it { is_expected.to be_truthy }
end
end
+
+ describe '#satisfied_by?' do
+ let(:pipeline) { build(:ci_pipeline, project: project, sha: project.repository.head_commit.sha) }
+
+ context 'when context is Build::Context::Build' do
+ it_behaves_like 'an exists rule with a context' do
+ let(:context) { Gitlab::Ci::Build::Context::Build.new(pipeline, sha: 'abc1234') }
+ end
+ end
+
+ context 'when context is Build::Context::Global' do
+ it_behaves_like 'an exists rule with a context' do
+ let(:context) { Gitlab::Ci::Build::Context::Global.new(pipeline, yaml_variables: {}) }
+ end
+ end
+
+ context 'when context is Config::External::Context' do
+ it_behaves_like 'an exists rule with a context' do
+ let(:context) { Gitlab::Ci::Config::External::Context.new(project: project, sha: project.repository.tree.sha) }
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb b/spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb
index b99048e2c18..0505b17ea91 100644
--- a/spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb
@@ -5,7 +5,7 @@ require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules::Rule do
let(:factory) do
Gitlab::Config::Entry::Factory.new(described_class)
- .value(config)
+ .value(config)
end
subject(:entry) { factory.create! }
@@ -25,6 +25,12 @@ RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules::Rule do
it { is_expected.to be_valid }
end
+ context 'when specifying an exists: clause' do
+ let(:config) { { exists: './this.md' } }
+
+ it { is_expected.to be_valid }
+ end
+
context 'using a list of multiple expressions' do
let(:config) { { if: ['$MY_VAR == "this"', '$YOUR_VAR == "that"'] } }
@@ -86,5 +92,13 @@ RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules::Rule do
expect(subject).to eq(if: '$THIS || $THAT')
end
end
+
+ context 'when specifying an exists: clause' do
+ let(:config) { { exists: './test.md' } }
+
+ it 'returns the config' do
+ expect(subject).to eq(exists: './test.md')
+ 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 b872f6644a2..c9c28e2eb8b 100644
--- a/spec/lib/gitlab/ci/config/entry/processable_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/processable_spec.rb
@@ -33,6 +33,14 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do
end
end
+ context 'when job name is more than 255' do
+ let(:entry) { node_class.new(config, name: ('a' * 256).to_sym) }
+
+ it 'shows a validation error' do
+ expect(entry.errors).to include "job name is too long (maximum is 255 characters)"
+ end
+ end
+
context 'when job name is empty' do
let(:entry) { node_class.new(config, name: ''.to_sym) }
diff --git a/spec/lib/gitlab/ci/config/extendable_spec.rb b/spec/lib/gitlab/ci/config/extendable_spec.rb
index 481f55d790e..2fc009569fc 100644
--- a/spec/lib/gitlab/ci/config/extendable_spec.rb
+++ b/spec/lib/gitlab/ci/config/extendable_spec.rb
@@ -73,6 +73,50 @@ RSpec.describe Gitlab::Ci::Config::Extendable do
end
end
+ context 'when the job tries to delete an extension key' do
+ let(:hash) do
+ {
+ something: {
+ script: 'deploy',
+ only: { variables: %w[$SOMETHING] }
+ },
+
+ test1: {
+ extends: 'something',
+ script: 'ls',
+ only: {}
+ },
+
+ test2: {
+ extends: 'something',
+ script: 'ls',
+ only: nil
+ }
+ }
+ end
+
+ it 'deletes the key if assigned to null' do
+ expect(subject.to_hash).to eq(
+ something: {
+ script: 'deploy',
+ only: { variables: %w[$SOMETHING] }
+ },
+ test1: {
+ extends: 'something',
+ script: 'ls',
+ only: {
+ variables: %w[$SOMETHING]
+ }
+ },
+ test2: {
+ extends: 'something',
+ script: 'ls',
+ only: nil
+ }
+ )
+ end
+ end
+
context 'when a hash uses recursive extensions' do
let(:hash) do
{
diff --git a/spec/lib/gitlab/ci/config/external/processor_spec.rb b/spec/lib/gitlab/ci/config/external/processor_spec.rb
index c2f28253f54..2e9e6f95071 100644
--- a/spec/lib/gitlab/ci/config/external/processor_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/processor_spec.rb
@@ -406,7 +406,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do
context 'when rules defined' do
context 'when a rule is invalid' do
let(:values) do
- { include: [{ local: 'builds.yml', rules: [{ exists: ['$MY_VAR'] }] }] }
+ { include: [{ local: 'builds.yml', rules: [{ changes: ['$MY_VAR'] }] }] }
end
it 'raises IncludeError' do
diff --git a/spec/lib/gitlab/ci/config/external/rules_spec.rb b/spec/lib/gitlab/ci/config/external/rules_spec.rb
index 9a5c29befa2..1e42cb30ae7 100644
--- a/spec/lib/gitlab/ci/config/external/rules_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/rules_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
RSpec.describe Gitlab::Ci::Config::External::Rules do
let(:rule_hashes) {}
@@ -32,6 +32,26 @@ RSpec.describe Gitlab::Ci::Config::External::Rules do
end
end
+ context 'when there is a rule with exists' do
+ let(:project) { create(:project, :repository) }
+ let(:context) { double(project: project, sha: project.repository.tree.sha, top_level_worktree_paths: ['test.md']) }
+ let(:rule_hashes) { [{ exists: 'Dockerfile' }] }
+
+ context 'when the file does not exist' do
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when the file exists' do
+ let(:context) { double(project: project, sha: project.repository.tree.sha, top_level_worktree_paths: ['Dockerfile']) }
+
+ before do
+ project.repository.create_file(project.owner, 'Dockerfile', "commit", message: 'test', branch_name: "master")
+ end
+
+ it { is_expected.to eq(true) }
+ end
+ end
+
context 'when there is a rule with if and when' do
let(:rule_hashes) { [{ if: '$MY_VAR == "hello"', when: 'on_success' }] }
@@ -41,12 +61,12 @@ RSpec.describe Gitlab::Ci::Config::External::Rules do
end
end
- context 'when there is a rule with exists' do
- let(:rule_hashes) { [{ exists: ['$MY_VAR'] }] }
+ context 'when there is a rule with changes' do
+ let(:rule_hashes) { [{ changes: ['$MY_VAR'] }] }
it 'raises an error' do
expect { result }.to raise_error(described_class::InvalidIncludeRulesError,
- 'invalid include rule: {:exists=>["$MY_VAR"]}')
+ 'invalid include rule: {:changes=>["$MY_VAR"]}')
end
end
end
diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb
index 3ec4519748f..1b3e8a2ce4a 100644
--- a/spec/lib/gitlab/ci/config_spec.rb
+++ b/spec/lib/gitlab/ci/config_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe Gitlab::Ci::Config do
end
let(:config) do
- described_class.new(yml, project: nil, sha: nil, user: nil)
+ described_class.new(yml, project: nil, pipeline: nil, sha: nil, user: nil)
end
context 'when config is valid' do
@@ -286,9 +286,12 @@ RSpec.describe Gitlab::Ci::Config do
end
context "when using 'include' directive" do
- let(:group) { create(:group) }
+ let_it_be(:group) { create(:group) }
+
let(:project) { create(:project, :repository, group: group) }
let(:main_project) { create(:project, :repository, :public, group: group) }
+ let(:pipeline) { build(:ci_pipeline, project: project) }
+
let(:remote_location) { 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.gitlab-ci-1.yml' }
let(:local_location) { 'spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml' }
@@ -327,7 +330,7 @@ RSpec.describe Gitlab::Ci::Config do
end
let(:config) do
- described_class.new(gitlab_ci_yml, project: project, sha: '12345', user: user)
+ described_class.new(gitlab_ci_yml, project: project, pipeline: pipeline, sha: '12345', user: user)
end
before do
@@ -594,7 +597,7 @@ RSpec.describe Gitlab::Ci::Config do
job1: {
script: ["echo 'hello from main file'"],
variables: {
- VARIABLE_DEFINED_IN_MAIN_FILE: 'some value'
+ VARIABLE_DEFINED_IN_MAIN_FILE: 'some value'
}
}
})
@@ -725,26 +728,91 @@ RSpec.describe Gitlab::Ci::Config do
end
context "when an 'include' has rules" do
+ context "when the rule is an if" do
+ let(:gitlab_ci_yml) do
+ <<~HEREDOC
+ include:
+ - local: #{local_location}
+ rules:
+ - if: $CI_PROJECT_ID == "#{project_id}"
+ image: ruby:2.7
+ HEREDOC
+ end
+
+ context 'when the rules condition is satisfied' do
+ let(:project_id) { project.id }
+
+ it 'includes the file' do
+ expect(config.to_hash).to include(local_location_hash)
+ end
+ end
+
+ context 'when the rules condition is satisfied' do
+ let(:project_id) { non_existing_record_id }
+
+ it 'does not include the file' do
+ expect(config.to_hash).not_to include(local_location_hash)
+ end
+ end
+ end
+
+ context "when the rule is an exists" do
+ let(:gitlab_ci_yml) do
+ <<~HEREDOC
+ include:
+ - local: #{local_location}
+ rules:
+ - exists: "#{filename}"
+ image: ruby:2.7
+ HEREDOC
+ end
+
+ before do
+ project.repository.create_file(
+ project.creator,
+ 'my_builds.yml',
+ local_file_content,
+ message: 'Add my_builds.yml',
+ branch_name: '12345'
+ )
+ end
+
+ context 'when the exists file does not exist' do
+ let(:filename) { 'not_a_real_file.md' }
+
+ it 'does not include the file' do
+ expect(config.to_hash).not_to include(local_location_hash)
+ end
+ end
+
+ context 'when the exists file does exist' do
+ let(:filename) { 'my_builds.yml' }
+
+ it 'does include the file' do
+ expect(config.to_hash).to include(local_location_hash)
+ end
+ end
+ end
+ end
+
+ context "when an 'include' has rules with a pipeline variable" do
let(:gitlab_ci_yml) do
<<~HEREDOC
include:
- local: #{local_location}
rules:
- - if: $CI_PROJECT_ID == "#{project_id}"
- image: ruby:2.7
+ - if: $CI_COMMIT_SHA == "#{project.commit.sha}"
HEREDOC
end
- context 'when the rules condition is satisfied' do
- let(:project_id) { project.id }
-
+ context 'when a pipeline is passed' do
it 'includes the file' do
expect(config.to_hash).to include(local_location_hash)
end
end
- context 'when the rules condition is satisfied' do
- let(:project_id) { non_existing_record_id }
+ context 'when a pipeline is not passed' do
+ let(:pipeline) { nil }
it 'does not include the file' do
expect(config.to_hash).not_to include(local_location_hash)
diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb
index 16517b39a45..cf21c98dbd5 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb
@@ -83,7 +83,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do
end
end
- it 'respects the defined payload schema' do
+ it 'respects the defined payload schema', :saas do
expect(::Gitlab::HTTP).to receive(:post) do |_url, params|
expect(params[:body]).to match_schema('/external_validation')
expect(params[:timeout]).to eq(described_class::DEFAULT_VALIDATION_REQUEST_TIMEOUT)
diff --git a/spec/lib/gitlab/ci/pipeline/quota/deployments_spec.rb b/spec/lib/gitlab/ci/pipeline/quota/deployments_spec.rb
index c52994fc6a2..5b0917c5c6f 100644
--- a/spec/lib/gitlab/ci/pipeline/quota/deployments_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/quota/deployments_spec.rb
@@ -3,9 +3,9 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Pipeline::Quota::Deployments do
- let_it_be(:namespace) { create(:namespace) }
- let_it_be(:default_plan, reload: true) { create(:default_plan) }
- let_it_be(:project, reload: true) { create(:project, :repository, namespace: namespace) }
+ let_it_be_with_refind(:namespace) { create(:namespace) }
+ let_it_be_with_reload(:default_plan) { create(:default_plan) }
+ let_it_be_with_reload(:project) { create(:project, :repository, namespace: namespace) }
let_it_be(:plan_limits) { create(:plan_limits, plan: default_plan) }
let(:pipeline) { build_stubbed(:ci_pipeline, project: project) }
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
index 3aa6b2e3c05..e2b64e65938 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
- let_it_be(:project) { create(:project, :repository) }
+ let_it_be_with_reload(:project) { create(:project, :repository) }
let_it_be(:head_sha) { project.repository.head_commit.id }
let(:pipeline) { build(:ci_empty_pipeline, project: project, sha: head_sha) }
@@ -13,7 +13,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
let(:previous_stages) { [] }
let(:current_stage) { double(seeds_names: [attributes[:name]]) }
- let(:seed_build) { described_class.new(seed_context, attributes, previous_stages, current_stage) }
+ let(:seed_build) { described_class.new(seed_context, attributes, previous_stages + [current_stage]) }
describe '#attributes' do
subject { seed_build.attributes }
@@ -393,12 +393,14 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
describe '#to_resource' do
subject { seed_build.to_resource }
- context 'when job is not a bridge' do
+ context 'when job is Ci::Build' do
it { is_expected.to be_a(::Ci::Build) }
it { is_expected.to be_valid }
shared_examples_for 'deployment job' do
it 'returns a job with deployment' do
+ expect { subject }.to change { Environment.count }.by(1)
+
expect(subject.deployment).not_to be_nil
expect(subject.deployment.deployable).to eq(subject)
expect(subject.deployment.environment.name).to eq(expected_environment_name)
@@ -413,6 +415,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
shared_examples_for 'ensures environment existence' do
it 'has environment' do
+ expect { subject }.to change { Environment.count }.by(1)
+
expect(subject).to be_has_environment
expect(subject.environment).to eq(environment_name)
expect(subject.metadata.expanded_environment_name).to eq(expected_environment_name)
@@ -422,6 +426,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
shared_examples_for 'ensures environment inexistence' do
it 'does not have environment' do
+ expect { subject }.not_to change { Environment.count }
+
expect(subject).not_to be_has_environment
expect(subject.environment).to be_nil
expect(subject.metadata&.expanded_environment_name).to be_nil
@@ -1212,14 +1218,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
]
end
- context 'when FF :variable_inside_variable is enabled' do
- before do
- stub_feature_flags(variable_inside_variable: [project])
- end
-
- it "does not have errors" do
- expect(subject.errors).to be_empty
- end
+ it "does not have errors" do
+ expect(subject.errors).to be_empty
end
end
@@ -1232,36 +1232,20 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
]
end
- context 'when FF :variable_inside_variable is disabled' do
- before do
- stub_feature_flags(variable_inside_variable: false)
- end
-
- it "does not have errors" do
- expect(subject.errors).to be_empty
- end
+ it "returns an error" do
+ expect(subject.errors).to contain_exactly(
+ 'rspec: circular variable reference detected: ["A", "B", "C"]')
end
- context 'when FF :variable_inside_variable is enabled' do
- before do
- stub_feature_flags(variable_inside_variable: [project])
- end
+ context 'with job:rules:[if:]' do
+ let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$C != null', when: 'always' }] } }
- it "returns an error" do
- expect(subject.errors).to contain_exactly(
- 'rspec: circular variable reference detected: ["A", "B", "C"]')
+ it "included? does not raise" do
+ expect { subject.included? }.not_to raise_error
end
- context 'with job:rules:[if:]' do
- let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$C != null', when: 'always' }] } }
-
- it "included? does not raise" do
- expect { subject.included? }.not_to raise_error
- end
-
- it "included? returns true" do
- expect(subject.included?).to eq(true)
- end
+ it "included? returns true" do
+ expect(subject.included?).to eq(true)
end
end
end
diff --git a/spec/lib/gitlab/ci/reports/security/report_spec.rb b/spec/lib/gitlab/ci/reports/security/report_spec.rb
index 5a85c3f19fc..a8b962ee970 100644
--- a/spec/lib/gitlab/ci/reports/security/report_spec.rb
+++ b/spec/lib/gitlab/ci/reports/security/report_spec.rb
@@ -221,4 +221,26 @@ RSpec.describe Gitlab::Ci::Reports::Security::Report do
end
end
end
+
+ describe '#has_signatures?' do
+ let(:finding) { create(:ci_reports_security_finding, signatures: signatures) }
+
+ subject { report.has_signatures? }
+
+ before do
+ report.add_finding(finding)
+ end
+
+ context 'when the findings of the report does not have signatures' do
+ let(:signatures) { [] }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when the findings of the report have signatures' do
+ let(:signatures) { [instance_double(Gitlab::Ci::Reports::Security::FindingSignature)] }
+
+ it { is_expected.to be_truthy }
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/reports/security/reports_spec.rb b/spec/lib/gitlab/ci/reports/security/reports_spec.rb
index 9b1e02f1418..79eee642552 100644
--- a/spec/lib/gitlab/ci/reports/security/reports_spec.rb
+++ b/spec/lib/gitlab/ci/reports/security/reports_spec.rb
@@ -54,11 +54,12 @@ RSpec.describe Gitlab::Ci::Reports::Security::Reports do
end
describe "#violates_default_policy_against?" do
- let(:high_severity_dast) { build(:ci_reports_security_finding, severity: 'high', report_type: :dast) }
+ let(:high_severity_dast) { build(:ci_reports_security_finding, severity: 'high', report_type: 'dast') }
let(:vulnerabilities_allowed) { 0 }
let(:severity_levels) { %w(critical high) }
+ let(:vulnerability_states) { %w(newly_detected)}
- subject { security_reports.violates_default_policy_against?(target_reports, vulnerabilities_allowed, severity_levels) }
+ subject { security_reports.violates_default_policy_against?(target_reports, vulnerabilities_allowed, severity_levels, vulnerability_states) }
before do
security_reports.get_report('sast', artifact).add_finding(high_severity_dast)
@@ -108,6 +109,22 @@ RSpec.describe Gitlab::Ci::Reports::Security::Reports do
it { is_expected.to be(false) }
end
+
+ context 'with related report_types' do
+ let(:report_types) { %w(dast sast) }
+
+ subject { security_reports.violates_default_policy_against?(target_reports, vulnerabilities_allowed, severity_levels, vulnerability_states, report_types) }
+
+ it { is_expected.to be(true) }
+ end
+
+ context 'with unrelated report_types' do
+ let(:report_types) { %w(dependency_scanning sast) }
+
+ subject { security_reports.violates_default_policy_against?(target_reports, vulnerabilities_allowed, severity_levels, vulnerability_states, report_types) }
+
+ it { is_expected.to be(false) }
+ end
end
end
end
diff --git a/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb
index d377cf0c735..789f694b4b4 100644
--- a/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb
@@ -27,9 +27,9 @@ RSpec.describe 'Jobs/Deploy.gitlab-ci.yml' do
end
describe 'the created pipeline' do
- let(:project) { create(:project, :repository) }
- let(:user) { project.owner }
+ let_it_be(:project, refind: true) { create(:project, :repository) }
+ let(:user) { project.owner }
let(:default_branch) { 'master' }
let(:pipeline_ref) { default_branch }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) }
@@ -43,23 +43,23 @@ RSpec.describe 'Jobs/Deploy.gitlab-ci.yml' do
allow(project).to receive(:default_branch).and_return(default_branch)
end
- context 'with no cluster' do
+ context 'with no cluster or agent' do
it 'does not create any kubernetes deployment jobs' do
expect(build_names).to eq %w(placeholder)
end
end
context 'with only a disabled cluster' do
- let!(:cluster) { create(:cluster, :project, :provided_by_gcp, enabled: false, projects: [project]) }
+ before do
+ create(:cluster, :project, :provided_by_gcp, enabled: false, projects: [project])
+ end
it 'does not create any kubernetes deployment jobs' do
expect(build_names).to eq %w(placeholder)
end
end
- context 'with an active cluster' do
- let!(:cluster) { create(:cluster, :project, :provided_by_gcp, projects: [project]) }
-
+ shared_examples_for 'pipeline with deployment jobs' do
context 'on master' do
it 'by default' do
expect(build_names).to include('production')
@@ -218,5 +218,21 @@ RSpec.describe 'Jobs/Deploy.gitlab-ci.yml' do
end
end
end
+
+ context 'with an agent' do
+ before do
+ create(:cluster_agent, project: project)
+ end
+
+ it_behaves_like 'pipeline with deployment jobs'
+ end
+
+ context 'with a cluster' do
+ before do
+ create(:cluster, :project, :provided_by_gcp, projects: [project])
+ end
+
+ it_behaves_like 'pipeline with deployment jobs'
+ end
end
end
diff --git a/spec/lib/gitlab/ci/templates/Jobs/sast_iac_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/sast_iac_gitlab_ci_yaml_spec.rb
new file mode 100644
index 00000000000..b9256ece78b
--- /dev/null
+++ b/spec/lib/gitlab/ci/templates/Jobs/sast_iac_gitlab_ci_yaml_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Jobs/SAST-IaC.latest.gitlab-ci.yml' do
+ subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Jobs/SAST-IaC.latest') }
+
+ describe 'the created pipeline' do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { project.owner }
+
+ let(:default_branch) { 'main' }
+ let(:pipeline_ref) { default_branch }
+ let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) }
+ let(:pipeline) { service.execute!(:push).payload }
+ let(:build_names) { pipeline.builds.pluck(:name) }
+
+ before do
+ stub_ci_pipeline_yaml_file(template.content)
+ allow_next_instance_of(Ci::BuildScheduleWorker) do |instance|
+ allow(instance).to receive(:perform).and_return(true)
+ end
+ allow(project).to receive(:default_branch).and_return(default_branch)
+ end
+
+ context 'on feature branch' do
+ let(:pipeline_ref) { 'feature' }
+
+ it 'creates the kics-iac-sast job' do
+ expect(build_names).to contain_exactly('kics-iac-sast')
+ end
+ end
+
+ context 'on merge request' do
+ let(:service) { MergeRequests::CreatePipelineService.new(project: project, current_user: user) }
+ let(:merge_request) { create(:merge_request, :simple, source_project: project) }
+ let(:pipeline) { service.execute(merge_request).payload }
+
+ it 'has no jobs' do
+ expect(pipeline).to be_merge_request_event
+ expect(build_names).to be_empty
+ end
+ end
+
+ context 'SAST_DISABLED is set' do
+ before do
+ create(:ci_variable, key: 'SAST_DISABLED', value: 'true', project: project)
+ end
+
+ context 'on default branch' do
+ it 'has no jobs' do
+ expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError)
+ end
+ end
+
+ context 'on feature branch' do
+ let(:pipeline_ref) { 'feature' }
+
+ it 'has no jobs' do
+ expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb
index 7602309627b..64ef6ecd7f8 100644
--- a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb
@@ -148,9 +148,7 @@ RSpec.describe 'Auto-DevOps.gitlab-ci.yml' do
it_behaves_like 'no Kubernetes deployment job'
end
- context 'when the project has an active cluster' do
- let!(:cluster) { create(:cluster, :project, :provided_by_gcp, projects: [project]) }
-
+ shared_examples 'pipeline with Kubernetes jobs' do
describe 'deployment-related builds' do
context 'on default branch' do
it 'does not include rollout jobs besides production' do
@@ -233,6 +231,22 @@ RSpec.describe 'Auto-DevOps.gitlab-ci.yml' do
end
end
end
+
+ context 'when a cluster is attached' do
+ before do
+ create(:cluster, :project, :provided_by_gcp, projects: [project])
+ end
+
+ it_behaves_like 'pipeline with Kubernetes jobs'
+ end
+
+ context 'when project has an Agent is present' do
+ before do
+ create(:cluster_agent, project: project)
+ end
+
+ it_behaves_like 'pipeline with Kubernetes jobs'
+ end
end
describe 'buildpack detection' do
diff --git a/spec/lib/gitlab/ci/templates/kaniko_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/kaniko_gitlab_ci_yaml_spec.rb
new file mode 100644
index 00000000000..c7dbbea4622
--- /dev/null
+++ b/spec/lib/gitlab/ci/templates/kaniko_gitlab_ci_yaml_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Kaniko.gitlab-ci.yml' do
+ subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Kaniko') }
+
+ describe 'the created pipeline' do
+ let(:pipeline_branch) { 'master' }
+ let(:project) { create(:project, :custom_repo, files: { 'Dockerfile' => 'FROM alpine:latest' }) }
+ let(:user) { project.owner }
+ let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
+ let(:pipeline) { service.execute!(:push).payload }
+ let(:build_names) { pipeline.builds.pluck(:name) }
+
+ before do
+ stub_ci_pipeline_yaml_file(template.content)
+ allow(Ci::BuildScheduleWorker).to receive(:perform).and_return(true)
+ end
+
+ it 'creates "kaniko-build" job' do
+ expect(build_names).to include('kaniko-build')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb
index 3d1306e82a5..fd5d5d6af7f 100644
--- a/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe 'Terraform.latest.gitlab-ci.yml' do
context 'on master branch' do
it 'creates init, validate and build jobs', :aggregate_failures do
expect(pipeline.errors).to be_empty
- expect(build_names).to include('init', 'validate', 'build', 'deploy')
+ expect(build_names).to include('validate', 'build', 'deploy')
end
end
diff --git a/spec/lib/gitlab/ci/trace/archive_spec.rb b/spec/lib/gitlab/ci/trace/archive_spec.rb
index c9fc4e720c4..5e965f94347 100644
--- a/spec/lib/gitlab/ci/trace/archive_spec.rb
+++ b/spec/lib/gitlab/ci/trace/archive_spec.rb
@@ -3,99 +3,134 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Trace::Archive do
- let_it_be(:job) { create(:ci_build, :success, :trace_live) }
- let_it_be_with_reload(:trace_metadata) { create(:ci_build_trace_metadata, build: job) }
- let_it_be(:src_checksum) do
- job.trace.read { |stream| Digest::MD5.hexdigest(stream.raw) }
- end
-
- let(:metrics) { spy('metrics') }
-
- describe '#execute' do
- subject { described_class.new(job, trace_metadata, metrics) }
-
- it 'computes and assigns checksum' do
- Gitlab::Ci::Trace::ChunkedIO.new(job) do |stream|
- expect { subject.execute!(stream) }.to change { Ci::JobArtifact.count }.by(1)
- end
-
- expect(trace_metadata.checksum).to eq(src_checksum)
- expect(trace_metadata.trace_artifact).to eq(job.job_artifacts_trace)
+ context 'with transactional fixtures' do
+ let_it_be(:job) { create(:ci_build, :success, :trace_live) }
+ let_it_be_with_reload(:trace_metadata) { create(:ci_build_trace_metadata, build: job) }
+ let_it_be(:src_checksum) do
+ job.trace.read { |stream| Digest::MD5.hexdigest(stream.raw) }
end
- context 'validating artifact checksum' do
- let(:trace) { 'abc' }
- let(:stream) { StringIO.new(trace, 'rb') }
- let(:src_checksum) { Digest::MD5.hexdigest(trace) }
+ let(:metrics) { spy('metrics') }
- context 'when the object store is disabled' do
- before do
- stub_artifacts_object_storage(enabled: false)
- end
-
- it 'skips validation' do
- subject.execute!(stream)
+ describe '#execute' do
+ subject { described_class.new(job, trace_metadata, metrics) }
- expect(trace_metadata.checksum).to eq(src_checksum)
- expect(trace_metadata.remote_checksum).to be_nil
- expect(metrics)
- .not_to have_received(:increment_error_counter)
- .with(type: :archive_invalid_checksum)
+ it 'computes and assigns checksum' do
+ Gitlab::Ci::Trace::ChunkedIO.new(job) do |stream|
+ expect { subject.execute!(stream) }.to change { Ci::JobArtifact.count }.by(1)
end
+
+ expect(trace_metadata.checksum).to eq(src_checksum)
+ expect(trace_metadata.trace_artifact).to eq(job.job_artifacts_trace)
end
- context 'with background_upload enabled' do
- before do
- stub_artifacts_object_storage(background_upload: true)
- end
+ context 'validating artifact checksum' do
+ let(:trace) { 'abc' }
+ let(:stream) { StringIO.new(trace, 'rb') }
+ let(:src_checksum) { Digest::MD5.hexdigest(trace) }
- it 'skips validation' do
- subject.execute!(stream)
+ context 'when the object store is disabled' do
+ before do
+ stub_artifacts_object_storage(enabled: false)
+ end
- expect(trace_metadata.checksum).to eq(src_checksum)
- expect(trace_metadata.remote_checksum).to be_nil
- expect(metrics)
- .not_to have_received(:increment_error_counter)
- .with(type: :archive_invalid_checksum)
+ it 'skips validation' do
+ subject.execute!(stream)
+ expect(trace_metadata.checksum).to eq(src_checksum)
+ expect(trace_metadata.remote_checksum).to be_nil
+ expect(metrics)
+ .not_to have_received(:increment_error_counter)
+ .with(error_reason: :archive_invalid_checksum)
+ end
end
- end
- context 'with direct_upload enabled' do
- before do
- stub_artifacts_object_storage(direct_upload: true)
- end
+ context 'with background_upload enabled' do
+ before do
+ stub_artifacts_object_storage(background_upload: true)
+ end
- it 'validates the archived trace' do
- subject.execute!(stream)
+ it 'skips validation' do
+ subject.execute!(stream)
- expect(trace_metadata.checksum).to eq(src_checksum)
- expect(trace_metadata.remote_checksum).to eq(src_checksum)
- expect(metrics)
- .not_to have_received(:increment_error_counter)
- .with(type: :archive_invalid_checksum)
+ expect(trace_metadata.checksum).to eq(src_checksum)
+ expect(trace_metadata.remote_checksum).to be_nil
+ expect(metrics)
+ .not_to have_received(:increment_error_counter)
+ .with(error_reason: :archive_invalid_checksum)
+ end
end
- context 'when the checksum does not match' do
- let(:invalid_remote_checksum) { SecureRandom.hex }
-
+ context 'with direct_upload enabled' do
before do
- expect(::Gitlab::Ci::Trace::RemoteChecksum)
- .to receive(:new)
- .with(an_instance_of(Ci::JobArtifact))
- .and_return(double(md5_checksum: invalid_remote_checksum))
+ stub_artifacts_object_storage(direct_upload: true)
end
it 'validates the archived trace' do
subject.execute!(stream)
expect(trace_metadata.checksum).to eq(src_checksum)
- expect(trace_metadata.remote_checksum).to eq(invalid_remote_checksum)
+ expect(trace_metadata.remote_checksum).to eq(src_checksum)
expect(metrics)
- .to have_received(:increment_error_counter)
- .with(type: :archive_invalid_checksum)
+ .not_to have_received(:increment_error_counter)
+ .with(error_reason: :archive_invalid_checksum)
+ end
+
+ context 'when the checksum does not match' do
+ let(:invalid_remote_checksum) { SecureRandom.hex }
+
+ before do
+ expect(::Gitlab::Ci::Trace::RemoteChecksum)
+ .to receive(:new)
+ .with(an_instance_of(Ci::JobArtifact))
+ .and_return(double(md5_checksum: invalid_remote_checksum))
+ end
+
+ it 'validates the archived trace' do
+ subject.execute!(stream)
+
+ expect(trace_metadata.checksum).to eq(src_checksum)
+ expect(trace_metadata.remote_checksum).to eq(invalid_remote_checksum)
+ expect(metrics)
+ .to have_received(:increment_error_counter)
+ .with(error_reason: :archive_invalid_checksum)
+ end
end
end
end
end
end
+
+ context 'without transactional fixtures', :delete do
+ let(:job) { create(:ci_build, :success, :trace_live) }
+ let(:trace_metadata) { create(:ci_build_trace_metadata, build: job) }
+ let(:stream) { StringIO.new('abc', 'rb') }
+
+ describe '#execute!' do
+ subject(:execute) do
+ ::Gitlab::Ci::Trace::Archive.new(job, trace_metadata).execute!(stream)
+ end
+
+ before do
+ stub_artifacts_object_storage(direct_upload: true)
+ end
+
+ it 'does not upload the trace inside a database transaction', :delete do
+ expect(Ci::ApplicationRecord.connection.transaction_open?).to be_falsey
+
+ allow_next_instance_of(Ci::JobArtifact) do |artifact|
+ artifact.job_id = job.id
+
+ expect(artifact)
+ .to receive(:store_file!)
+ .and_wrap_original do |store_method, *args|
+ expect(Ci::ApplicationRecord.connection.transaction_open?).to be_falsey
+
+ store_method.call(*args)
+ end
+ end
+
+ execute
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/trace/metrics_spec.rb b/spec/lib/gitlab/ci/trace/metrics_spec.rb
index 53e55a57973..733ffbbea22 100644
--- a/spec/lib/gitlab/ci/trace/metrics_spec.rb
+++ b/spec/lib/gitlab/ci/trace/metrics_spec.rb
@@ -17,23 +17,23 @@ RSpec.describe Gitlab::Ci::Trace::Metrics, :prometheus do
end
describe '#increment_error_counter' do
- context 'when the operation type is known' do
+ context 'when the error reason is known' do
it 'increments the counter' do
- subject.increment_error_counter(type: :chunks_invalid_size)
- subject.increment_error_counter(type: :chunks_invalid_checksum)
- subject.increment_error_counter(type: :archive_invalid_checksum)
+ subject.increment_error_counter(error_reason: :chunks_invalid_size)
+ subject.increment_error_counter(error_reason: :chunks_invalid_checksum)
+ subject.increment_error_counter(error_reason: :archive_invalid_checksum)
- expect(described_class.trace_errors_counter.get(type: :chunks_invalid_size)).to eq 1
- expect(described_class.trace_errors_counter.get(type: :chunks_invalid_checksum)).to eq 1
- expect(described_class.trace_errors_counter.get(type: :archive_invalid_checksum)).to eq 1
+ expect(described_class.trace_errors_counter.get(error_reason: :chunks_invalid_size)).to eq 1
+ expect(described_class.trace_errors_counter.get(error_reason: :chunks_invalid_checksum)).to eq 1
+ expect(described_class.trace_errors_counter.get(error_reason: :archive_invalid_checksum)).to eq 1
expect(described_class.trace_errors_counter.values.count).to eq 3
end
end
- context 'when the operation type is known' do
+ context 'when the error reason is unknown' do
it 'raises an exception' do
- expect { subject.increment_error_counter(type: :invalid_type) }
+ expect { subject.increment_error_counter(error_reason: :invalid_type) }
.to raise_error(ArgumentError)
end
end
diff --git a/spec/lib/gitlab/ci/trace_spec.rb b/spec/lib/gitlab/ci/trace_spec.rb
index 1a31b2dad56..888ceb7ff9a 100644
--- a/spec/lib/gitlab/ci/trace_spec.rb
+++ b/spec/lib/gitlab/ci/trace_spec.rb
@@ -25,16 +25,6 @@ RSpec.describe Gitlab::Ci::Trace, :clean_gitlab_redis_shared_state, factory_defa
artifact1.file.migrate!(ObjectStorage::Store::REMOTE)
end
- it 'reloads the trace after is it migrated' do
- stub_const('Gitlab::HttpIO::BUFFER_SIZE', test_data.length)
-
- expect_next_instance_of(Gitlab::HttpIO) do |http_io|
- expect(http_io).to receive(:get_chunk).and_return(test_data, "")
- end
-
- expect(artifact2.job.trace.raw).to eq(test_data)
- end
-
it 'reloads the trace in case of a chunk error' do
chunk_error = described_class::ChunkedIO::FailedToGetChunkError
diff --git a/spec/lib/gitlab/ci/variables/builder_spec.rb b/spec/lib/gitlab/ci/variables/builder_spec.rb
new file mode 100644
index 00000000000..10275f33484
--- /dev/null
+++ b/spec/lib/gitlab/ci/variables/builder_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Variables::Builder do
+ let(:builder) { described_class.new(pipeline) }
+ let(:pipeline) { create(:ci_pipeline) }
+ let(:job) { create(:ci_build, pipeline: pipeline) }
+
+ describe '#scoped_variables' do
+ let(:environment) { job.expanded_environment_name }
+ let(:dependencies) { true }
+
+ subject { builder.scoped_variables(job, environment: environment, dependencies: dependencies) }
+
+ it 'returns the expected variables' do
+ keys = %w[CI_JOB_NAME
+ CI_JOB_STAGE
+ CI_NODE_TOTAL
+ CI_BUILD_NAME
+ CI_BUILD_STAGE]
+
+ subject.map { |env| env[:key] }.tap do |names|
+ expect(names).to include(*keys)
+ end
+ end
+
+ context 'feature flag disabled' do
+ before do
+ stub_feature_flags(ci_predefined_vars_in_builder: false)
+ end
+
+ it 'returns no variables' do
+ expect(subject.map { |env| env[:key] }).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/variables/collection_spec.rb b/spec/lib/gitlab/ci/variables/collection_spec.rb
index 7ba98380986..26c560565e0 100644
--- a/spec/lib/gitlab/ci/variables/collection_spec.rb
+++ b/spec/lib/gitlab/ci/variables/collection_spec.rb
@@ -358,302 +358,210 @@ RSpec.describe Gitlab::Ci::Variables::Collection do
end
describe '#sort_and_expand_all' 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: [project_with_flag_enabled])
- end
+ context 'table tests' do
+ using RSpec::Parameterized::TableSyntax
- context 'table tests' do
- using RSpec::Parameterized::TableSyntax
-
- where do
- {
- "empty array": {
- variables: [],
- keep_undefined: false
- },
- "simple expansions": {
- variables: [
- { key: 'variable', value: 'value' },
- { key: 'variable2', value: 'result' },
- { key: 'variable3', value: 'key$variable$variable2' }
- ],
- keep_undefined: false
- },
- "complex expansion": {
- variables: [
- { key: 'variable', value: 'value' },
- { key: 'variable2', value: 'key${variable}' }
- ],
- keep_undefined: false
- },
- "out-of-order variable reference": {
- variables: [
- { key: 'variable2', value: 'key${variable}' },
- { key: 'variable', value: 'value' }
- ],
- keep_undefined: false
- },
- "complex expansions with raw variable": {
- variables: [
- { key: 'variable3', value: 'key_${variable}_${variable2}' },
- { key: 'variable', value: '$variable2', raw: true },
- { key: 'variable2', value: 'value2' }
- ],
- keep_undefined: false
- },
- "escaped characters in complex expansions are kept intact": {
- variables: [
- { key: 'variable3', value: 'key_${variable}_$${HOME}_%%HOME%%' },
- { key: 'variable', value: '$variable2' },
- { key: 'variable2', value: 'value2' }
- ],
- keep_undefined: false
- },
- "array with cyclic dependency": {
- variables: [
- { key: 'variable', value: '$variable2' },
- { key: 'variable2', value: '$variable3' },
- { key: 'variable3', value: 'key$variable$variable2' }
- ],
- keep_undefined: true
- }
+ where do
+ {
+ "empty array": {
+ variables: [],
+ keep_undefined: false,
+ result: []
+ },
+ "simple expansions": {
+ variables: [
+ { key: 'variable', value: 'value' },
+ { key: 'variable2', value: 'result' },
+ { key: 'variable3', value: 'key$variable$variable2' },
+ { key: 'variable4', value: 'key$variable$variable3' }
+ ],
+ keep_undefined: false,
+ result: [
+ { key: 'variable', value: 'value' },
+ { key: 'variable2', value: 'result' },
+ { key: 'variable3', value: 'keyvalueresult' },
+ { key: 'variable4', value: 'keyvaluekeyvalueresult' }
+ ]
+ },
+ "complex expansion": {
+ variables: [
+ { key: 'variable', value: 'value' },
+ { key: 'variable2', value: 'key${variable}' }
+ ],
+ keep_undefined: false,
+ result: [
+ { key: 'variable', value: 'value' },
+ { key: 'variable2', value: 'keyvalue' }
+ ]
+ },
+ "unused variables": {
+ variables: [
+ { key: 'variable', value: 'value' },
+ { key: 'variable2', value: 'result2' },
+ { key: 'variable3', value: 'result3' },
+ { key: 'variable4', value: 'key$variable$variable3' }
+ ],
+ keep_undefined: false,
+ result: [
+ { key: 'variable', value: 'value' },
+ { key: 'variable2', value: 'result2' },
+ { key: 'variable3', value: 'result3' },
+ { key: 'variable4', value: 'keyvalueresult3' }
+ ]
+ },
+ "complex expansions": {
+ variables: [
+ { key: 'variable', value: 'value' },
+ { key: 'variable2', value: 'result' },
+ { key: 'variable3', value: 'key${variable}${variable2}' }
+ ],
+ keep_undefined: false,
+ result: [
+ { key: 'variable', value: 'value' },
+ { key: 'variable2', value: 'result' },
+ { key: 'variable3', value: 'keyvalueresult' }
+ ]
+ },
+ "escaped characters in complex expansions keeping undefined are kept intact": {
+ variables: [
+ { key: 'variable3', value: 'key_${variable}_$${HOME}_%%HOME%%' },
+ { key: 'variable', value: '$variable2' },
+ { key: 'variable2', value: 'value' }
+ ],
+ keep_undefined: true,
+ result: [
+ { key: 'variable', value: 'value' },
+ { key: 'variable2', value: 'value' },
+ { key: 'variable3', value: 'key_value_$${HOME}_%%HOME%%' }
+ ]
+ },
+ "escaped characters in complex expansions discarding undefined are kept intact": {
+ variables: [
+ { key: 'variable2', value: 'key_${variable4}_$${HOME}_%%HOME%%' },
+ { key: 'variable', value: 'value_$${HOME}_%%HOME%%' }
+ ],
+ keep_undefined: false,
+ result: [
+ { key: 'variable', value: 'value_$${HOME}_%%HOME%%' },
+ { key: 'variable2', value: 'key__$${HOME}_%%HOME%%' }
+ ]
+ },
+ "out-of-order expansion": {
+ variables: [
+ { key: 'variable3', value: 'key$variable2$variable' },
+ { key: 'variable', value: 'value' },
+ { key: 'variable2', value: 'result' }
+ ],
+ keep_undefined: false,
+ result: [
+ { key: 'variable2', value: 'result' },
+ { key: 'variable', value: 'value' },
+ { key: 'variable3', value: 'keyresultvalue' }
+ ]
+ },
+ "out-of-order complex expansion": {
+ variables: [
+ { key: 'variable', value: 'value' },
+ { key: 'variable2', value: 'result' },
+ { key: 'variable3', value: 'key${variable2}${variable}' }
+ ],
+ keep_undefined: false,
+ result: [
+ { key: 'variable', value: 'value' },
+ { key: 'variable2', value: 'result' },
+ { key: 'variable3', value: 'keyresultvalue' }
+ ]
+ },
+ "missing variable discarding original": {
+ variables: [
+ { key: 'variable2', value: 'key$variable' }
+ ],
+ keep_undefined: false,
+ result: [
+ { key: 'variable2', value: 'key' }
+ ]
+ },
+ "missing variable keeping original": {
+ variables: [
+ { key: 'variable2', value: 'key$variable' }
+ ],
+ keep_undefined: true,
+ result: [
+ { key: 'variable2', value: 'key$variable' }
+ ]
+ },
+ "complex expansions with missing variable keeping original": {
+ variables: [
+ { key: 'variable4', value: 'key${variable}${variable2}${variable3}' },
+ { key: 'variable', value: 'value' },
+ { key: 'variable3', value: 'value3' }
+ ],
+ keep_undefined: true,
+ result: [
+ { key: 'variable', value: 'value' },
+ { key: 'variable3', value: 'value3' },
+ { key: 'variable4', value: 'keyvalue${variable2}value3' }
+ ]
+ },
+ "complex expansions with raw variable": {
+ variables: [
+ { key: 'variable3', value: 'key_${variable}_${variable2}' },
+ { key: 'variable', value: '$variable2', raw: true },
+ { key: 'variable2', value: 'value2' }
+ ],
+ keep_undefined: false,
+ result: [
+ { key: 'variable', value: '$variable2', raw: true },
+ { key: 'variable2', value: 'value2' },
+ { key: 'variable3', value: 'key_$variable2_value2' }
+ ]
+ },
+ "variable value referencing password with special characters": {
+ variables: [
+ { key: 'VAR', value: '$PASSWORD' },
+ { key: 'PASSWORD', value: 'my_password$$_%%_$A' },
+ { key: 'A', value: 'value' }
+ ],
+ keep_undefined: false,
+ result: [
+ { key: 'VAR', value: 'my_password$$_%%_value' },
+ { key: 'PASSWORD', value: 'my_password$$_%%_value' },
+ { key: 'A', value: 'value' }
+ ]
+ },
+ "cyclic dependency causes original array to be returned": {
+ variables: [
+ { key: 'variable', value: '$variable2' },
+ { key: 'variable2', value: '$variable3' },
+ { key: 'variable3', value: 'key$variable$variable2' }
+ ],
+ keep_undefined: false,
+ result: [
+ { key: 'variable', value: '$variable2' },
+ { key: 'variable2', value: '$variable3' },
+ { key: 'variable3', value: 'key$variable$variable2' }
+ ]
}
- end
-
- with_them do
- let(:collection) { Gitlab::Ci::Variables::Collection.new(variables, keep_undefined: keep_undefined) }
-
- subject { collection.sort_and_expand_all(project_with_flag_disabled) }
-
- it 'returns Collection' do
- is_expected.to be_an_instance_of(Gitlab::Ci::Variables::Collection)
- end
-
- it 'does not expand variables' do
- var_hash = variables.pluck(:key, :value).to_h
- expect(subject.to_hash).to eq(var_hash)
- end
- end
+ }
end
- 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) }
+ with_them do
+ let(:collection) { Gitlab::Ci::Variables::Collection.new(variables) }
- before do
- stub_feature_flags(variable_inside_variable: [project_with_flag_enabled])
- end
+ subject { collection.sort_and_expand_all(keep_undefined: keep_undefined) }
- context 'table tests' do
- using RSpec::Parameterized::TableSyntax
-
- where do
- {
- "empty array": {
- variables: [],
- keep_undefined: false,
- result: []
- },
- "simple expansions": {
- variables: [
- { key: 'variable', value: 'value' },
- { key: 'variable2', value: 'result' },
- { key: 'variable3', value: 'key$variable$variable2' },
- { key: 'variable4', value: 'key$variable$variable3' }
- ],
- keep_undefined: false,
- result: [
- { key: 'variable', value: 'value' },
- { key: 'variable2', value: 'result' },
- { key: 'variable3', value: 'keyvalueresult' },
- { key: 'variable4', value: 'keyvaluekeyvalueresult' }
- ]
- },
- "complex expansion": {
- variables: [
- { key: 'variable', value: 'value' },
- { key: 'variable2', value: 'key${variable}' }
- ],
- keep_undefined: false,
- result: [
- { key: 'variable', value: 'value' },
- { key: 'variable2', value: 'keyvalue' }
- ]
- },
- "unused variables": {
- variables: [
- { key: 'variable', value: 'value' },
- { key: 'variable2', value: 'result2' },
- { key: 'variable3', value: 'result3' },
- { key: 'variable4', value: 'key$variable$variable3' }
- ],
- keep_undefined: false,
- result: [
- { key: 'variable', value: 'value' },
- { key: 'variable2', value: 'result2' },
- { key: 'variable3', value: 'result3' },
- { key: 'variable4', value: 'keyvalueresult3' }
- ]
- },
- "complex expansions": {
- variables: [
- { key: 'variable', value: 'value' },
- { key: 'variable2', value: 'result' },
- { key: 'variable3', value: 'key${variable}${variable2}' }
- ],
- keep_undefined: false,
- result: [
- { key: 'variable', value: 'value' },
- { key: 'variable2', value: 'result' },
- { key: 'variable3', value: 'keyvalueresult' }
- ]
- },
- "escaped characters in complex expansions keeping undefined are kept intact": {
- variables: [
- { key: 'variable3', value: 'key_${variable}_$${HOME}_%%HOME%%' },
- { key: 'variable', value: '$variable2' },
- { key: 'variable2', value: 'value' }
- ],
- keep_undefined: true,
- result: [
- { key: 'variable', value: 'value' },
- { key: 'variable2', value: 'value' },
- { key: 'variable3', value: 'key_value_$${HOME}_%%HOME%%' }
- ]
- },
- "escaped characters in complex expansions discarding undefined are kept intact": {
- variables: [
- { key: 'variable2', value: 'key_${variable4}_$${HOME}_%%HOME%%' },
- { key: 'variable', value: 'value_$${HOME}_%%HOME%%' }
- ],
- keep_undefined: false,
- result: [
- { key: 'variable', value: 'value_$${HOME}_%%HOME%%' },
- { key: 'variable2', value: 'key__$${HOME}_%%HOME%%' }
- ]
- },
- "out-of-order expansion": {
- variables: [
- { key: 'variable3', value: 'key$variable2$variable' },
- { key: 'variable', value: 'value' },
- { key: 'variable2', value: 'result' }
- ],
- keep_undefined: false,
- result: [
- { key: 'variable2', value: 'result' },
- { key: 'variable', value: 'value' },
- { key: 'variable3', value: 'keyresultvalue' }
- ]
- },
- "out-of-order complex expansion": {
- variables: [
- { key: 'variable', value: 'value' },
- { key: 'variable2', value: 'result' },
- { key: 'variable3', value: 'key${variable2}${variable}' }
- ],
- keep_undefined: false,
- result: [
- { key: 'variable', value: 'value' },
- { key: 'variable2', value: 'result' },
- { key: 'variable3', value: 'keyresultvalue' }
- ]
- },
- "missing variable discarding original": {
- variables: [
- { key: 'variable2', value: 'key$variable' }
- ],
- keep_undefined: false,
- result: [
- { key: 'variable2', value: 'key' }
- ]
- },
- "missing variable keeping original": {
- variables: [
- { key: 'variable2', value: 'key$variable' }
- ],
- keep_undefined: true,
- result: [
- { key: 'variable2', value: 'key$variable' }
- ]
- },
- "complex expansions with missing variable keeping original": {
- variables: [
- { key: 'variable4', value: 'key${variable}${variable2}${variable3}' },
- { key: 'variable', value: 'value' },
- { key: 'variable3', value: 'value3' }
- ],
- keep_undefined: true,
- result: [
- { key: 'variable', value: 'value' },
- { key: 'variable3', value: 'value3' },
- { key: 'variable4', value: 'keyvalue${variable2}value3' }
- ]
- },
- "complex expansions with raw variable": {
- variables: [
- { key: 'variable3', value: 'key_${variable}_${variable2}' },
- { key: 'variable', value: '$variable2', raw: true },
- { key: 'variable2', value: 'value2' }
- ],
- keep_undefined: false,
- result: [
- { key: 'variable', value: '$variable2', raw: true },
- { key: 'variable2', value: 'value2' },
- { key: 'variable3', value: 'key_$variable2_value2' }
- ]
- },
- "variable value referencing password with special characters": {
- variables: [
- { key: 'VAR', value: '$PASSWORD' },
- { key: 'PASSWORD', value: 'my_password$$_%%_$A' },
- { key: 'A', value: 'value' }
- ],
- keep_undefined: false,
- result: [
- { key: 'VAR', value: 'my_password$$_%%_value' },
- { key: 'PASSWORD', value: 'my_password$$_%%_value' },
- { key: 'A', value: 'value' }
- ]
- },
- "cyclic dependency causes original array to be returned": {
- variables: [
- { key: 'variable', value: '$variable2' },
- { key: 'variable2', value: '$variable3' },
- { key: 'variable3', value: 'key$variable$variable2' }
- ],
- keep_undefined: false,
- result: [
- { key: 'variable', value: '$variable2' },
- { key: 'variable2', value: '$variable3' },
- { key: 'variable3', value: 'key$variable$variable2' }
- ]
- }
- }
+ it 'returns Collection' do
+ is_expected.to be_an_instance_of(Gitlab::Ci::Variables::Collection)
end
- with_them do
- let(:collection) { Gitlab::Ci::Variables::Collection.new(variables) }
-
- subject { collection.sort_and_expand_all(project_with_flag_enabled, keep_undefined: keep_undefined) }
-
- it 'returns Collection' do
- is_expected.to be_an_instance_of(Gitlab::Ci::Variables::Collection)
- end
-
- it 'expands variables' do
- var_hash = result.to_h { |env| [env.fetch(:key), env.fetch(:value)] }
- .with_indifferent_access
- expect(subject.to_hash).to eq(var_hash)
- end
+ it 'expands variables' do
+ var_hash = result.to_h { |env| [env.fetch(:key), env.fetch(:value)] }
+ .with_indifferent_access
+ expect(subject.to_hash).to eq(var_hash)
+ end
- it 'preserves raw attribute' do
- expect(subject.pluck(:key, :raw).to_h).to eq(collection.pluck(:key, :raw).to_h)
- end
+ it 'preserves raw attribute' do
+ expect(subject.pluck(:key, :raw).to_h).to eq(collection.pluck(:key, :raw).to_h)
end
end
end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index 1591c2e6b60..f00a801286d 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -1046,6 +1046,64 @@ module Gitlab
end
end
+ context 'when overriding `extends`' do
+ let(:config) do
+ <<~YAML
+ .base:
+ script: test
+ variables:
+ VAR1: base var 1
+
+ test1:
+ extends: .base
+ variables:
+ VAR1: test1 var 1
+ VAR2: test2 var 2
+
+ test2:
+ extends: .base
+ variables:
+ VAR2: test2 var 2
+
+ test3:
+ extends: .base
+ variables: {}
+
+ test4:
+ extends: .base
+ variables: null
+ YAML
+ end
+
+ it 'correctly extends jobs' do
+ expect(config_processor.builds[0]).to include(
+ name: 'test1',
+ options: { script: ['test'] },
+ job_variables: [{ key: 'VAR1', value: 'test1 var 1', public: true },
+ { key: 'VAR2', value: 'test2 var 2', public: true }]
+ )
+
+ expect(config_processor.builds[1]).to include(
+ name: 'test2',
+ options: { script: ['test'] },
+ job_variables: [{ key: 'VAR1', value: 'base var 1', public: true },
+ { key: 'VAR2', value: 'test2 var 2', public: true }]
+ )
+
+ expect(config_processor.builds[2]).to include(
+ name: 'test3',
+ options: { script: ['test'] },
+ job_variables: [{ key: 'VAR1', value: 'base var 1', public: true }]
+ )
+
+ expect(config_processor.builds[3]).to include(
+ name: 'test4',
+ options: { script: ['test'] },
+ job_variables: []
+ )
+ end
+ end
+
context 'when using recursive `extends`' do
let(:config) do
<<~YAML
diff --git a/spec/lib/gitlab/config_checker/external_database_checker_spec.rb b/spec/lib/gitlab/config_checker/external_database_checker_spec.rb
index 5a4e9001ac9..933b6d6be9e 100644
--- a/spec/lib/gitlab/config_checker/external_database_checker_spec.rb
+++ b/spec/lib/gitlab/config_checker/external_database_checker_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Gitlab::ConfigChecker::ExternalDatabaseChecker do
context 'when database meets minimum supported version' do
before do
- allow(Gitlab::Database.main).to receive(:postgresql_minimum_supported_version?).and_return(true)
+ allow(ApplicationRecord.database).to receive(:postgresql_minimum_supported_version?).and_return(true)
end
it { is_expected.to be_empty }
@@ -16,7 +16,7 @@ RSpec.describe Gitlab::ConfigChecker::ExternalDatabaseChecker do
context 'when database does not meet minimum supported version' do
before do
- allow(Gitlab::Database.main).to receive(:postgresql_minimum_supported_version?).and_return(false)
+ allow(ApplicationRecord.database).to receive(:postgresql_minimum_supported_version?).and_return(false)
end
let(:notice_deprecated_database) do
@@ -26,7 +26,7 @@ RSpec.describe Gitlab::ConfigChecker::ExternalDatabaseChecker do
'%{pg_version_minimum} is required for this version of GitLab. ' \
'Please upgrade your environment to a supported PostgreSQL version, ' \
'see %{pg_requirements_url} for details.') % {
- pg_version_current: Gitlab::Database.main.version,
+ pg_version_current: ApplicationRecord.database.version,
pg_version_minimum: Gitlab::Database::MINIMUM_POSTGRES_VERSION,
pg_requirements_url: '<a href="https://docs.gitlab.com/ee/install/requirements.html#database">database requirements</a>'
}
diff --git a/spec/services/projects/container_repository/cache_tags_created_at_service_spec.rb b/spec/lib/gitlab/container_repository/tags/cache_spec.rb
index dfe2ff9e57c..f84c1ce173f 100644
--- a/spec/services/projects/container_repository/cache_tags_created_at_service_spec.rb
+++ b/spec/lib/gitlab/container_repository/tags/cache_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Projects::ContainerRepository::CacheTagsCreatedAtService, :clean_gitlab_redis_cache do
+RSpec.describe ::Gitlab::ContainerRepository::Tags::Cache, :clean_gitlab_redis_cache do
let_it_be(:dummy_tag_class) { Struct.new(:name, :created_at) }
let_it_be(:repository) { create(:container_repository) }
diff --git a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
index 3ec332dace5..c0476d38380 100644
--- a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
+++ b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
@@ -50,7 +50,7 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
expect(directives.has_key?('report_uri')).to be_truthy
expect(directives['report_uri']).to be_nil
- expect(directives['child_src']).to eq(directives['frame_src'])
+ expect(directives['child_src']).to eq("#{directives['frame_src']} #{directives['worker_src']}")
end
context 'adds all websocket origins to support Safari' do
@@ -77,13 +77,15 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
context 'when CDN host is defined' do
before do
- stub_config_setting(cdn_host: 'https://example.com')
+ stub_config_setting(cdn_host: 'https://cdn.example.com')
end
it 'adds CDN host to CSP' do
- expect(directives['script_src']).to eq("'strict-dynamic' 'self' 'unsafe-inline' 'unsafe-eval' https://www.google.com/recaptcha/ https://www.recaptcha.net https://apis.google.com https://example.com")
- expect(directives['style_src']).to eq("'self' 'unsafe-inline' https://example.com")
- expect(directives['font_src']).to eq("'self' https://example.com")
+ expect(directives['script_src']).to eq(::Gitlab::ContentSecurityPolicy::Directives.script_src + " https://cdn.example.com")
+ expect(directives['style_src']).to eq("'self' 'unsafe-inline' https://cdn.example.com")
+ expect(directives['font_src']).to eq("'self' https://cdn.example.com")
+ expect(directives['worker_src']).to eq('http://localhost/assets/ blob: data: https://cdn.example.com')
+ expect(directives['frame_src']).to eq(::Gitlab::ContentSecurityPolicy::Directives.frame_src + " https://cdn.example.com http://localhost/admin/sidekiq http://localhost/admin/sidekiq/ http://localhost/-/speedscope/index.html")
end
end
@@ -99,8 +101,10 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
end
context 'when CUSTOMER_PORTAL_URL is set' do
+ let(:customer_portal_url) { 'https://customers.example.com' }
+
before do
- stub_env('CUSTOMER_PORTAL_URL', 'https://customers.example.com')
+ stub_env('CUSTOMER_PORTAL_URL', customer_portal_url)
end
context 'when in production' do
@@ -109,7 +113,7 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
end
it 'does not add CUSTOMER_PORTAL_URL to CSP' do
- expect(directives['frame_src']).to eq("'self' https://www.google.com/recaptcha/ https://www.recaptcha.net/ https://content.googleapis.com https://content-compute.googleapis.com https://content-cloudbilling.googleapis.com https://content-cloudresourcemanager.googleapis.com")
+ expect(directives['frame_src']).to eq(::Gitlab::ContentSecurityPolicy::Directives.frame_src + " http://localhost/admin/sidekiq http://localhost/admin/sidekiq/ http://localhost/-/speedscope/index.html")
end
end
@@ -119,7 +123,36 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
end
it 'adds CUSTOMER_PORTAL_URL to CSP' do
- expect(directives['frame_src']).to eq("'self' https://www.google.com/recaptcha/ https://www.recaptcha.net/ https://content.googleapis.com https://content-compute.googleapis.com https://content-cloudbilling.googleapis.com https://content-cloudresourcemanager.googleapis.com https://customers.example.com")
+ expect(directives['frame_src']).to eq(::Gitlab::ContentSecurityPolicy::Directives.frame_src + " http://localhost/rails/letter_opener/ https://customers.example.com http://localhost/admin/sidekiq http://localhost/admin/sidekiq/ http://localhost/-/speedscope/index.html")
+ end
+ end
+ end
+
+ context 'letter_opener applicaiton URL' do
+ let(:gitlab_url) { 'http://gitlab.example.com' }
+ let(:letter_opener_url) { "#{gitlab_url}/rails/letter_opener/" }
+
+ before do
+ stub_config_setting(url: gitlab_url)
+ end
+
+ context 'when in production' do
+ before do
+ allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production'))
+ end
+
+ it 'does not add letter_opener to CSP' do
+ expect(directives['frame_src']).not_to include(letter_opener_url)
+ end
+ end
+
+ context 'when in development' do
+ before do
+ allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('development'))
+ end
+
+ it 'adds letter_opener to CSP' do
+ expect(directives['frame_src']).to include(letter_opener_url)
end
end
end
diff --git a/spec/lib/gitlab/contributions_calendar_spec.rb b/spec/lib/gitlab/contributions_calendar_spec.rb
index 67b2ea7a1d4..384609c6664 100644
--- a/spec/lib/gitlab/contributions_calendar_spec.rb
+++ b/spec/lib/gitlab/contributions_calendar_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::ContributionsCalendar do
let(:contributor) { create(:user) }
let(:user) { create(:user) }
+ let(:travel_time) { nil }
let(:private_project) do
create(:project, :private) do |project|
@@ -31,7 +32,7 @@ RSpec.describe Gitlab::ContributionsCalendar do
let(:last_year) { today - 1.year }
before do
- travel_to Time.now.utc.end_of_day
+ travel_to travel_time || Time.now.utc.end_of_day
end
after do
@@ -89,7 +90,7 @@ RSpec.describe Gitlab::ContributionsCalendar do
expect(calendar(contributor).activity_dates[today]).to eq(2)
end
- context "when events fall under different dates depending on the time zone" do
+ context "when events fall under different dates depending on the system time zone" do
before do
create_event(public_project, today, 1)
create_event(public_project, today, 4)
@@ -116,6 +117,37 @@ RSpec.describe Gitlab::ContributionsCalendar do
end
end
end
+
+ context "when events fall under different dates depending on the contributor's time zone" do
+ before do
+ create_event(public_project, today, 1)
+ create_event(public_project, today, 4)
+ create_event(public_project, today, 10)
+ create_event(public_project, today, 16)
+ create_event(public_project, today, 23)
+ end
+
+ it "renders correct event counts within the UTC timezone" do
+ Time.use_zone('UTC') do
+ contributor.timezone = 'UTC'
+ expect(calendar.activity_dates).to eq(today => 5)
+ end
+ end
+
+ it "renders correct event counts within the Sydney timezone" do
+ Time.use_zone('UTC') do
+ contributor.timezone = 'Sydney'
+ expect(calendar.activity_dates).to eq(today => 3, tomorrow => 2)
+ end
+ end
+
+ it "renders correct event counts within the US Central timezone" do
+ Time.use_zone('UTC') do
+ contributor.timezone = 'Central Time (US & Canada)'
+ expect(calendar.activity_dates).to eq(yesterday => 2, today => 3)
+ end
+ end
+ end
end
describe '#events_by_date' do
@@ -152,14 +184,38 @@ RSpec.describe Gitlab::ContributionsCalendar do
end
describe '#starting_year' do
- it "is the start of last year" do
- expect(calendar.starting_year).to eq(last_year.year)
+ let(:travel_time) { Time.find_zone('UTC').local(2020, 12, 31, 19, 0, 0) }
+
+ context "when the contributor's timezone is not set" do
+ it "is the start of last year in the system timezone" do
+ expect(calendar.starting_year).to eq(2019)
+ end
+ end
+
+ context "when the contributor's timezone is set to Sydney" do
+ let(:contributor) { create(:user, { timezone: 'Sydney' }) }
+
+ it "is the start of last year in Sydney" do
+ expect(calendar.starting_year).to eq(2020)
+ end
end
end
describe '#starting_month' do
- it "is the start of this month" do
- expect(calendar.starting_month).to eq(today.month)
+ let(:travel_time) { Time.find_zone('UTC').local(2020, 12, 31, 19, 0, 0) }
+
+ context "when the contributor's timezone is not set" do
+ it "is the start of this month in the system timezone" do
+ expect(calendar.starting_month).to eq(12)
+ end
+ end
+
+ context "when the contributor's timezone is set to Sydney" do
+ let(:contributor) { create(:user, { timezone: 'Sydney' }) }
+
+ it "is the start of this month in Sydney" do
+ expect(calendar.starting_month).to eq(1)
+ end
end
end
end
diff --git a/spec/lib/gitlab/database/async_indexes/postgres_async_index_spec.rb b/spec/lib/gitlab/database/async_indexes/postgres_async_index_spec.rb
index 434cba4edde..223730f87c0 100644
--- a/spec/lib/gitlab/database/async_indexes/postgres_async_index_spec.rb
+++ b/spec/lib/gitlab/database/async_indexes/postgres_async_index_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Gitlab::Database::AsyncIndexes::PostgresAsyncIndex, type: :model do
+ it { is_expected.to be_a Gitlab::Database::SharedModel }
+
describe 'validations' do
let(:identifier_limit) { described_class::MAX_IDENTIFIER_LENGTH }
let(:definition_limit) { described_class::MAX_DEFINITION_LENGTH }
diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb
index 779e8e40c97..04c18a98ee6 100644
--- a/spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb
@@ -286,7 +286,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do
let(:migration_wrapper) { Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper.new }
let(:migration_helpers) { ActiveRecord::Migration.new }
- let(:table_name) { :_batched_migrations_test_table }
+ let(:table_name) { :_test_batched_migrations_test_table }
let(:column_name) { :some_id }
let(:job_arguments) { [:some_id, :some_id_convert_to_bigint] }
diff --git a/spec/lib/gitlab/database/batch_count_spec.rb b/spec/lib/gitlab/database/batch_count_spec.rb
index da13bc425d1..9831510f014 100644
--- a/spec/lib/gitlab/database/batch_count_spec.rb
+++ b/spec/lib/gitlab/database/batch_count_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe Gitlab::Database::BatchCount do
end
before do
- allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(in_transaction)
+ allow(model.connection).to receive(:transaction_open?).and_return(in_transaction)
end
def calculate_batch_size(batch_size)
diff --git a/spec/lib/gitlab/database/connection_spec.rb b/spec/lib/gitlab/database/connection_spec.rb
deleted file mode 100644
index ee1df141cd6..00000000000
--- a/spec/lib/gitlab/database/connection_spec.rb
+++ /dev/null
@@ -1,442 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Database::Connection do
- let(:connection) { described_class.new }
-
- describe '#config' do
- it 'returns a HashWithIndifferentAccess' do
- expect(connection.config).to be_an_instance_of(HashWithIndifferentAccess)
- end
-
- it 'returns a default pool size' do
- expect(connection.config)
- .to include(pool: Gitlab::Database.default_pool_size)
- end
-
- it 'does not cache its results' do
- a = connection.config
- b = connection.config
-
- expect(a).not_to equal(b)
- end
- end
-
- describe '#pool_size' do
- context 'when no explicit size is configured' do
- it 'returns the default pool size' do
- expect(connection).to receive(:config).and_return({ pool: nil })
-
- expect(connection.pool_size).to eq(Gitlab::Database.default_pool_size)
- end
- end
-
- context 'when an explicit pool size is set' do
- it 'returns the pool size' do
- expect(connection).to receive(:config).and_return({ pool: 4 })
-
- expect(connection.pool_size).to eq(4)
- end
- end
- end
-
- describe '#username' do
- context 'when a username is set' do
- it 'returns the username' do
- allow(connection).to receive(:config).and_return(username: 'bob')
-
- expect(connection.username).to eq('bob')
- end
- end
-
- context 'when a username is not set' do
- it 'returns the value of the USER environment variable' do
- allow(connection).to receive(:config).and_return(username: nil)
- allow(ENV).to receive(:[]).with('USER').and_return('bob')
-
- expect(connection.username).to eq('bob')
- end
- end
- end
-
- describe '#database_name' do
- it 'returns the name of the database' do
- allow(connection).to receive(:config).and_return(database: 'test')
-
- expect(connection.database_name).to eq('test')
- end
- end
-
- describe '#adapter_name' do
- it 'returns the database adapter name' do
- allow(connection).to receive(:config).and_return(adapter: 'test')
-
- expect(connection.adapter_name).to eq('test')
- end
- end
-
- describe '#human_adapter_name' do
- context 'when the adapter is PostgreSQL' do
- it 'returns PostgreSQL' do
- allow(connection).to receive(:config).and_return(adapter: 'postgresql')
-
- expect(connection.human_adapter_name).to eq('PostgreSQL')
- end
- end
-
- context 'when the adapter is not PostgreSQL' do
- it 'returns Unknown' do
- allow(connection).to receive(:config).and_return(adapter: 'kittens')
-
- expect(connection.human_adapter_name).to eq('Unknown')
- end
- end
- end
-
- describe '#postgresql?' do
- context 'when using PostgreSQL' do
- it 'returns true' do
- allow(connection).to receive(:adapter_name).and_return('PostgreSQL')
-
- expect(connection.postgresql?).to eq(true)
- end
- end
-
- context 'when not using PostgreSQL' do
- it 'returns false' do
- allow(connection).to receive(:adapter_name).and_return('MySQL')
-
- expect(connection.postgresql?).to eq(false)
- end
- end
- end
-
- describe '#db_config_with_default_pool_size' do
- it 'returns db_config with our default pool size' do
- allow(Gitlab::Database).to receive(:default_pool_size).and_return(9)
-
- expect(connection.db_config_with_default_pool_size.pool).to eq(9)
- end
-
- it 'returns db_config with the correct database name' do
- db_name = connection.scope.connection.pool.db_config.name
-
- expect(connection.db_config_with_default_pool_size.name).to eq(db_name)
- end
- end
-
- describe '#disable_prepared_statements', :reestablished_active_record_base do
- it 'disables prepared statements' do
- connection.scope.establish_connection(
- ::Gitlab::Database.main.config.merge(prepared_statements: true)
- )
-
- expect(connection.scope.connection.prepared_statements).to eq(true)
-
- connection.disable_prepared_statements
-
- expect(connection.scope.connection.prepared_statements).to eq(false)
- end
-
- it 'retains the connection name' do
- connection.disable_prepared_statements
-
- expect(connection.scope.connection_db_config.name).to eq('main')
- end
-
- context 'with dynamic connection pool size' do
- before do
- connection.scope.establish_connection(connection.config.merge(pool: 7))
- end
-
- it 'retains the set pool size' do
- connection.disable_prepared_statements
-
- expect(connection.scope.connection.prepared_statements).to eq(false)
- expect(connection.scope.connection.pool.size).to eq(7)
- end
- end
- end
-
- describe '#db_read_only?' do
- it 'detects a read-only database' do
- allow(connection.scope.connection)
- .to receive(:execute)
- .with('SELECT pg_is_in_recovery()')
- .and_return([{ "pg_is_in_recovery" => "t" }])
-
- expect(connection.db_read_only?).to be_truthy
- end
-
- it 'detects a read-only database' do
- allow(connection.scope.connection)
- .to receive(:execute)
- .with('SELECT pg_is_in_recovery()')
- .and_return([{ "pg_is_in_recovery" => true }])
-
- expect(connection.db_read_only?).to be_truthy
- end
-
- it 'detects a read-write database' do
- allow(connection.scope.connection)
- .to receive(:execute)
- .with('SELECT pg_is_in_recovery()')
- .and_return([{ "pg_is_in_recovery" => "f" }])
-
- expect(connection.db_read_only?).to be_falsey
- end
-
- it 'detects a read-write database' do
- allow(connection.scope.connection)
- .to receive(:execute)
- .with('SELECT pg_is_in_recovery()')
- .and_return([{ "pg_is_in_recovery" => false }])
-
- expect(connection.db_read_only?).to be_falsey
- end
- end
-
- describe '#db_read_write?' do
- it 'detects a read-only database' do
- allow(connection.scope.connection)
- .to receive(:execute)
- .with('SELECT pg_is_in_recovery()')
- .and_return([{ "pg_is_in_recovery" => "t" }])
-
- expect(connection.db_read_write?).to eq(false)
- end
-
- it 'detects a read-only database' do
- allow(connection.scope.connection)
- .to receive(:execute)
- .with('SELECT pg_is_in_recovery()')
- .and_return([{ "pg_is_in_recovery" => true }])
-
- expect(connection.db_read_write?).to eq(false)
- end
-
- it 'detects a read-write database' do
- allow(connection.scope.connection)
- .to receive(:execute)
- .with('SELECT pg_is_in_recovery()')
- .and_return([{ "pg_is_in_recovery" => "f" }])
-
- expect(connection.db_read_write?).to eq(true)
- end
-
- it 'detects a read-write database' do
- allow(connection.scope.connection)
- .to receive(:execute)
- .with('SELECT pg_is_in_recovery()')
- .and_return([{ "pg_is_in_recovery" => false }])
-
- expect(connection.db_read_write?).to eq(true)
- end
- end
-
- describe '#version' do
- around do |example|
- connection.instance_variable_set(:@version, nil)
- example.run
- connection.instance_variable_set(:@version, nil)
- end
-
- context "on postgresql" do
- it "extracts the version number" do
- allow(connection)
- .to receive(:database_version)
- .and_return("PostgreSQL 9.4.4 on x86_64-apple-darwin14.3.0")
-
- expect(connection.version).to eq '9.4.4'
- end
- end
-
- it 'memoizes the result' do
- count = ActiveRecord::QueryRecorder
- .new { 2.times { connection.version } }
- .count
-
- expect(count).to eq(1)
- end
- end
-
- describe '#postgresql_minimum_supported_version?' do
- it 'returns false when using PostgreSQL 10' do
- allow(connection).to receive(:version).and_return('10')
-
- expect(connection.postgresql_minimum_supported_version?).to eq(false)
- end
-
- it 'returns false when using PostgreSQL 11' do
- allow(connection).to receive(:version).and_return('11')
-
- expect(connection.postgresql_minimum_supported_version?).to eq(false)
- end
-
- it 'returns true when using PostgreSQL 12' do
- allow(connection).to receive(:version).and_return('12')
-
- expect(connection.postgresql_minimum_supported_version?).to eq(true)
- end
- end
-
- describe '#bulk_insert' do
- before do
- allow(connection).to receive(:connection).and_return(dummy_connection)
- allow(dummy_connection).to receive(:quote_column_name, &:itself)
- allow(dummy_connection).to receive(:quote, &:itself)
- allow(dummy_connection).to receive(:execute)
- end
-
- let(:dummy_connection) { double(:connection) }
-
- let(:rows) do
- [
- { a: 1, b: 2, c: 3 },
- { c: 6, a: 4, b: 5 }
- ]
- end
-
- it 'does nothing with empty rows' do
- expect(dummy_connection).not_to receive(:execute)
-
- connection.bulk_insert('test', [])
- end
-
- it 'uses the ordering from the first row' do
- expect(dummy_connection).to receive(:execute) do |sql|
- expect(sql).to include('(1, 2, 3)')
- expect(sql).to include('(4, 5, 6)')
- end
-
- connection.bulk_insert('test', rows)
- end
-
- it 'quotes column names' do
- expect(dummy_connection).to receive(:quote_column_name).with(:a)
- expect(dummy_connection).to receive(:quote_column_name).with(:b)
- expect(dummy_connection).to receive(:quote_column_name).with(:c)
-
- connection.bulk_insert('test', rows)
- end
-
- it 'quotes values' do
- 1.upto(6) do |i|
- expect(dummy_connection).to receive(:quote).with(i)
- end
-
- connection.bulk_insert('test', rows)
- end
-
- it 'does not quote values of a column in the disable_quote option' do
- [1, 2, 4, 5].each do |i|
- expect(dummy_connection).to receive(:quote).with(i)
- end
-
- connection.bulk_insert('test', rows, disable_quote: :c)
- end
-
- it 'does not quote values of columns in the disable_quote option' do
- [2, 5].each do |i|
- expect(dummy_connection).to receive(:quote).with(i)
- end
-
- connection.bulk_insert('test', rows, disable_quote: [:a, :c])
- end
-
- it 'handles non-UTF-8 data' do
- expect { connection.bulk_insert('test', [{ a: "\255" }]) }.not_to raise_error
- end
-
- context 'when using PostgreSQL' do
- it 'allows the returning of the IDs of the inserted rows' do
- result = double(:result, values: [['10']])
-
- expect(dummy_connection)
- .to receive(:execute)
- .with(/RETURNING id/)
- .and_return(result)
-
- ids = connection
- .bulk_insert('test', [{ number: 10 }], return_ids: true)
-
- expect(ids).to eq([10])
- end
-
- it 'allows setting the upsert to do nothing' do
- expect(dummy_connection)
- .to receive(:execute)
- .with(/ON CONFLICT DO NOTHING/)
-
- connection
- .bulk_insert('test', [{ number: 10 }], on_conflict: :do_nothing)
- end
- end
- end
-
- describe '#cached_column_exists?' do
- it 'only retrieves the data from the schema cache' do
- queries = ActiveRecord::QueryRecorder.new do
- 2.times do
- expect(connection.cached_column_exists?(:projects, :id)).to be_truthy
- expect(connection.cached_column_exists?(:projects, :bogus_column)).to be_falsey
- end
- end
-
- expect(queries.count).to eq(0)
- end
- end
-
- describe '#cached_table_exists?' do
- it 'only retrieves the data from the schema cache' do
- queries = ActiveRecord::QueryRecorder.new do
- 2.times do
- expect(connection.cached_table_exists?(:projects)).to be_truthy
- expect(connection.cached_table_exists?(:bogus_table_name)).to be_falsey
- end
- end
-
- expect(queries.count).to eq(0)
- end
-
- it 'returns false when database does not exist' do
- expect(connection.scope).to receive(:connection) do
- raise ActiveRecord::NoDatabaseError, 'broken'
- end
-
- expect(connection.cached_table_exists?(:projects)).to be(false)
- end
- end
-
- describe '#exists?' do
- it 'returns true if the database exists' do
- expect(connection.exists?).to be(true)
- end
-
- it "returns false if the database doesn't exist" do
- expect(connection.scope.connection.schema_cache)
- .to receive(:database_version)
- .and_raise(ActiveRecord::NoDatabaseError)
-
- expect(connection.exists?).to be(false)
- end
- end
-
- describe '#system_id' do
- it 'returns the PostgreSQL system identifier' do
- expect(connection.system_id).to be_an_instance_of(Integer)
- end
- end
-
- describe '#get_write_location' do
- it 'returns a string' do
- expect(connection.get_write_location(connection.scope.connection))
- .to be_a(String)
- end
-
- it 'returns nil if there are no results' do
- expect(connection.get_write_location(double(select_all: []))).to be_nil
- end
- end
-end
diff --git a/spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb b/spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb
index cdcc862c376..9d49db1f018 100644
--- a/spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb
+++ b/spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb
@@ -38,7 +38,8 @@ RSpec.describe Gitlab::Database::Count::ReltuplesCountStrategy do
it 'returns nil counts for inherited tables' do
models.each { |model| expect(model).not_to receive(:count) }
- expect(subject).to eq({ Namespace => 3 })
+ # 3 Namespaces as parents for each Project and 3 ProjectNamespaces(for each Project)
+ expect(subject).to eq({ Namespace => 6 })
end
end
diff --git a/spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb b/spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb
index c2028f8c238..2f261aebf02 100644
--- a/spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb
+++ b/spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb
@@ -47,7 +47,8 @@ RSpec.describe Gitlab::Database::Count::TablesampleCountStrategy do
result = subject
expect(result[Project]).to eq(3)
expect(result[Group]).to eq(1)
- expect(result[Namespace]).to eq(4)
+ # 1-Group, 3 namespaces for each project and 3 project namespaces for each project
+ expect(result[Namespace]).to eq(7)
end
end
diff --git a/spec/lib/gitlab/database/each_database_spec.rb b/spec/lib/gitlab/database/each_database_spec.rb
new file mode 100644
index 00000000000..9327fc4ff78
--- /dev/null
+++ b/spec/lib/gitlab/database/each_database_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::EachDatabase do
+ describe '.each_database_connection' do
+ let(:expected_connections) do
+ Gitlab::Database.database_base_models.map { |name, model| [model.connection, name] }
+ end
+
+ it 'yields each connection after connecting SharedModel' do
+ expected_connections.each do |connection, _|
+ expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(connection).and_yield
+ end
+
+ yielded_connections = []
+
+ described_class.each_database_connection do |connection, name|
+ yielded_connections << [connection, name]
+ end
+
+ expect(yielded_connections).to match_array(expected_connections)
+ end
+ end
+
+ describe '.each_model_connection' do
+ let(:model1) { double(connection: double, table_name: 'table1') }
+ let(:model2) { double(connection: double, table_name: 'table2') }
+
+ before do
+ allow(model1.connection).to receive_message_chain('pool.db_config.name').and_return('name1')
+ allow(model2.connection).to receive_message_chain('pool.db_config.name').and_return('name2')
+ end
+
+ it 'yields each model after connecting SharedModel' do
+ expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(model1.connection).and_yield
+ expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(model2.connection).and_yield
+
+ yielded_models = []
+
+ described_class.each_model_connection([model1, model2]) do |model, name|
+ yielded_models << [model, name]
+ end
+
+ expect(yielded_models).to match_array([[model1, 'name1'], [model2, 'name2']])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/gitlab_schema_spec.rb b/spec/lib/gitlab/database/gitlab_schema_spec.rb
new file mode 100644
index 00000000000..255efc99ff6
--- /dev/null
+++ b/spec/lib/gitlab/database/gitlab_schema_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::GitlabSchema do
+ describe '.tables_to_schema' do
+ subject { described_class.tables_to_schema }
+
+ it 'all tables have assigned a known gitlab_schema' do
+ is_expected.to all(
+ match([be_a(String), be_in([:gitlab_shared, :gitlab_main, :gitlab_ci])])
+ )
+ end
+
+ # This being run across different databases indirectly also tests
+ # a general consistency of structure across databases
+ Gitlab::Database.database_base_models.each do |db_config_name, db_class|
+ let(:db_data_sources) { db_class.connection.data_sources }
+
+ context "for #{db_config_name} using #{db_class}" do
+ it 'new data sources are added' do
+ missing_tables = db_data_sources.to_set - subject.keys
+
+ expect(missing_tables).to be_empty, \
+ "Missing table(s) #{missing_tables.to_a} not found in #{described_class}.tables_to_schema. " \
+ "Any new tables must be added to lib/gitlab/database/gitlab_schemas.yml."
+ end
+
+ it 'non-existing data sources are removed' do
+ extra_tables = subject.keys.to_set - db_data_sources
+
+ expect(extra_tables).to be_empty, \
+ "Extra table(s) #{extra_tables.to_a} found in #{described_class}.tables_to_schema. " \
+ "Any removed or renamed tables must be removed from lib/gitlab/database/gitlab_schemas.yml."
+ end
+ end
+ end
+ end
+
+ describe '.table_schema' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:name, :classification) do
+ 'ci_builds' | :gitlab_ci
+ 'my_schema.ci_builds' | :gitlab_ci
+ 'information_schema.columns' | :gitlab_shared
+ 'audit_events_part_5fc467ac26' | :gitlab_main
+ '_test_my_table' | :gitlab_shared
+ 'pg_attribute' | :gitlab_shared
+ 'my_other_table' | :undefined_my_other_table
+ end
+
+ with_them do
+ subject { described_class.table_schema(name) }
+
+ it { is_expected.to eq(classification) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/load_balancing/configuration_spec.rb b/spec/lib/gitlab/database/load_balancing/configuration_spec.rb
index 3e5249a3dea..eef248afdf2 100644
--- a/spec/lib/gitlab/database/load_balancing/configuration_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/configuration_spec.rb
@@ -3,17 +3,12 @@
require 'spec_helper'
RSpec.describe Gitlab::Database::LoadBalancing::Configuration do
- let(:model) do
- config = ActiveRecord::DatabaseConfigurations::HashConfig
- .new('main', 'test', configuration_hash)
-
- double(:model, connection_db_config: config)
- end
+ let(:configuration_hash) { {} }
+ let(:db_config) { ActiveRecord::DatabaseConfigurations::HashConfig.new('test', 'ci', configuration_hash) }
+ let(:model) { double(:model, connection_db_config: db_config) }
describe '.for_model' do
context 'when load balancing is not configured' do
- let(:configuration_hash) { {} }
-
it 'uses the default settings' do
config = described_class.for_model(model)
@@ -105,6 +100,14 @@ RSpec.describe Gitlab::Database::LoadBalancing::Configuration do
expect(config.pool_size).to eq(4)
end
end
+
+ it 'calls reuse_primary_connection!' do
+ expect_next_instance_of(described_class) do |subject|
+ expect(subject).to receive(:reuse_primary_connection!).and_call_original
+ end
+
+ described_class.for_model(model)
+ end
end
describe '#load_balancing_enabled?' do
@@ -180,4 +183,60 @@ RSpec.describe Gitlab::Database::LoadBalancing::Configuration do
end
end
end
+
+ describe '#db_config_name' do
+ let(:config) { described_class.new(model) }
+
+ subject { config.db_config_name }
+
+ it 'returns connection name as symbol' do
+ is_expected.to eq(:ci)
+ end
+ end
+
+ describe '#replica_db_config' do
+ let(:model) { double(:model, connection_db_config: db_config, connection_specification_name: 'Ci::ApplicationRecord') }
+ let(:config) { described_class.for_model(model) }
+
+ it 'returns exactly db_config' do
+ expect(config.replica_db_config).to eq(db_config)
+ end
+
+ context 'when GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci=main' do
+ it 'does not change replica_db_config' do
+ stub_env('GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci', 'main')
+
+ expect(config.replica_db_config).to eq(db_config)
+ end
+ end
+ end
+
+ describe 'reuse_primary_connection!' do
+ let(:model) { double(:model, connection_db_config: db_config, connection_specification_name: 'Ci::ApplicationRecord') }
+ let(:config) { described_class.for_model(model) }
+
+ context 'when GITLAB_LOAD_BALANCING_REUSE_PRIMARY_* not configured' do
+ it 'the primary connection uses default specification' do
+ stub_env('GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci', nil)
+
+ expect(config.primary_connection_specification_name).to eq('Ci::ApplicationRecord')
+ end
+ end
+
+ context 'when GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci=main' do
+ it 'the primary connection uses main connection' do
+ stub_env('GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci', 'main')
+
+ expect(config.primary_connection_specification_name).to eq('ActiveRecord::Base')
+ end
+ end
+
+ context 'when GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci=unknown' do
+ it 'raises exception' do
+ stub_env('GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci', 'unknown')
+
+ expect { config.reuse_primary_connection! }.to raise_error /Invalid value for/
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/load_balancing/connection_proxy_spec.rb b/spec/lib/gitlab/database/load_balancing/connection_proxy_spec.rb
index ba2f9485066..ee2718171c0 100644
--- a/spec/lib/gitlab/database/load_balancing/connection_proxy_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/connection_proxy_spec.rb
@@ -3,12 +3,9 @@
require 'spec_helper'
RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do
- let(:proxy) do
- config = Gitlab::Database::LoadBalancing::Configuration
- .new(ActiveRecord::Base)
-
- described_class.new(Gitlab::Database::LoadBalancing::LoadBalancer.new(config))
- end
+ let(:config) { Gitlab::Database::LoadBalancing::Configuration.new(ActiveRecord::Base) }
+ let(:load_balancer) { Gitlab::Database::LoadBalancing::LoadBalancer.new(config) }
+ let(:proxy) { described_class.new(load_balancer) }
describe '#select' do
it 'performs a read' do
@@ -85,7 +82,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do
describe '.insert_all!' do
before do
ActiveRecord::Schema.define do
- create_table :connection_proxy_bulk_insert, force: true do |t|
+ create_table :_test_connection_proxy_bulk_insert, force: true do |t|
t.string :name, null: true
end
end
@@ -93,13 +90,13 @@ RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do
after do
ActiveRecord::Schema.define do
- drop_table :connection_proxy_bulk_insert, force: true
+ drop_table :_test_connection_proxy_bulk_insert, force: true
end
end
let(:model_class) do
Class.new(ApplicationRecord) do
- self.table_name = "connection_proxy_bulk_insert"
+ self.table_name = "_test_connection_proxy_bulk_insert"
end
end
@@ -143,9 +140,9 @@ RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do
context 'with a read query' do
it 'runs the transaction and any nested queries on the replica' do
- expect(proxy.load_balancer).to receive(:read)
+ expect(load_balancer).to receive(:read)
.twice.and_yield(replica)
- expect(proxy.load_balancer).not_to receive(:read_write)
+ expect(load_balancer).not_to receive(:read_write)
expect(session).not_to receive(:write!)
proxy.transaction { proxy.select('true') }
@@ -154,8 +151,8 @@ RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do
context 'with a write query' do
it 'raises an exception' do
- allow(proxy.load_balancer).to receive(:read).and_yield(replica)
- allow(proxy.load_balancer).to receive(:read_write).and_yield(replica)
+ allow(load_balancer).to receive(:read).and_yield(replica)
+ allow(load_balancer).to receive(:read_write).and_yield(replica)
expect do
proxy.transaction { proxy.insert('something') }
@@ -178,9 +175,9 @@ RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do
context 'with a read query' do
it 'runs the transaction and any nested queries on the primary and stick to it' do
- expect(proxy.load_balancer).to receive(:read_write)
+ expect(load_balancer).to receive(:read_write)
.twice.and_yield(primary)
- expect(proxy.load_balancer).not_to receive(:read)
+ expect(load_balancer).not_to receive(:read)
expect(session).to receive(:write!)
proxy.transaction { proxy.select('true') }
@@ -189,9 +186,9 @@ RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do
context 'with a write query' do
it 'runs the transaction and any nested queries on the primary and stick to it' do
- expect(proxy.load_balancer).to receive(:read_write)
+ expect(load_balancer).to receive(:read_write)
.twice.and_yield(primary)
- expect(proxy.load_balancer).not_to receive(:read)
+ expect(load_balancer).not_to receive(:read)
expect(session).to receive(:write!).twice
proxy.transaction { proxy.insert('something') }
@@ -209,7 +206,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do
end
it 'properly forwards keyword arguments' do
- allow(proxy.load_balancer).to receive(:read_write)
+ allow(load_balancer).to receive(:read_write)
expect(proxy).to receive(:write_using_load_balancer).and_call_original
@@ -234,7 +231,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do
end
it 'properly forwards keyword arguments' do
- allow(proxy.load_balancer).to receive(:read)
+ allow(load_balancer).to receive(:read)
expect(proxy).to receive(:read_using_load_balancer).and_call_original
@@ -259,7 +256,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do
allow(session).to receive(:use_replicas_for_read_queries?).and_return(false)
expect(connection).to receive(:foo).with('foo')
- expect(proxy.load_balancer).to receive(:read).and_yield(connection)
+ expect(load_balancer).to receive(:read).and_yield(connection)
proxy.read_using_load_balancer(:foo, 'foo')
end
@@ -271,7 +268,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do
allow(session).to receive(:use_replicas_for_read_queries?).and_return(true)
expect(connection).to receive(:foo).with('foo')
- expect(proxy.load_balancer).to receive(:read).and_yield(connection)
+ expect(load_balancer).to receive(:read).and_yield(connection)
proxy.read_using_load_balancer(:foo, 'foo')
end
@@ -283,7 +280,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do
allow(session).to receive(:use_replicas_for_read_queries?).and_return(true)
expect(connection).to receive(:foo).with('foo')
- expect(proxy.load_balancer).to receive(:read).and_yield(connection)
+ expect(load_balancer).to receive(:read).and_yield(connection)
proxy.read_using_load_balancer(:foo, 'foo')
end
@@ -296,7 +293,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do
expect(connection).to receive(:foo).with('foo')
- expect(proxy.load_balancer).to receive(:read_write)
+ expect(load_balancer).to receive(:read_write)
.and_yield(connection)
proxy.read_using_load_balancer(:foo, 'foo')
@@ -314,7 +311,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do
end
it 'uses but does not stick to the primary' do
- expect(proxy.load_balancer).to receive(:read_write).and_yield(connection)
+ expect(load_balancer).to receive(:read_write).and_yield(connection)
expect(connection).to receive(:foo).with('foo')
expect(session).not_to receive(:write!)
diff --git a/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb b/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb
index f824d4cefdf..37b83729125 100644
--- a/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb
@@ -4,10 +4,11 @@ require 'spec_helper'
RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store do
let(:conflict_error) { Class.new(RuntimeError) }
- let(:db_host) { ActiveRecord::Base.connection_pool.db_config.host }
+ let(:model) { ActiveRecord::Base }
+ let(:db_host) { model.connection_pool.db_config.host }
let(:config) do
Gitlab::Database::LoadBalancing::Configuration
- .new(ActiveRecord::Base, [db_host, db_host])
+ .new(model, [db_host, db_host])
end
let(:lb) { described_class.new(config) }
@@ -88,6 +89,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store do
host = double(:host)
allow(lb).to receive(:host).and_return(host)
+ allow(Rails.application.executor).to receive(:active?).and_return(true)
allow(host).to receive(:query_cache_enabled).and_return(false)
allow(host).to receive(:connection).and_return(connection)
@@ -96,6 +98,20 @@ RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store do
lb.read { 10 }
end
+ it 'does not enable query cache when outside Rails executor context' do
+ connection = double(:connection)
+ host = double(:host)
+
+ allow(lb).to receive(:host).and_return(host)
+ allow(Rails.application.executor).to receive(:active?).and_return(false)
+ allow(host).to receive(:query_cache_enabled).and_return(false)
+ allow(host).to receive(:connection).and_return(connection)
+
+ expect(host).not_to receive(:enable_query_cache!)
+
+ lb.read { 10 }
+ end
+
it 'marks hosts that are offline' do
allow(lb).to receive(:connection_error?).and_return(true)
@@ -216,7 +232,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store do
it 'does not create conflicts with other load balancers when caching hosts' do
ci_config = Gitlab::Database::LoadBalancing::Configuration
- .new(Ci::CiDatabaseRecord, [db_host, db_host])
+ .new(Ci::ApplicationRecord, [db_host, db_host])
lb1 = described_class.new(config)
lb2 = described_class.new(ci_config)
@@ -459,4 +475,84 @@ RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store do
lb.disconnect!(timeout: 30)
end
end
+
+ describe '#get_write_location' do
+ it 'returns a string' do
+ expect(lb.send(:get_write_location, lb.pool.connection))
+ .to be_a(String)
+ end
+
+ it 'returns nil if there are no results' do
+ expect(lb.send(:get_write_location, double(select_all: []))).to be_nil
+ end
+ end
+
+ describe 'primary connection re-use', :reestablished_active_record_base do
+ let(:model) { Ci::ApplicationRecord }
+
+ around do |example|
+ if Gitlab::Database.has_config?(:ci)
+ example.run
+ else
+ # fake additional Database
+ model.establish_connection(
+ ActiveRecord::DatabaseConfigurations::HashConfig.new(Rails.env, 'ci', ActiveRecord::Base.connection_db_config.configuration_hash)
+ )
+
+ example.run
+
+ # Cleanup connection_specification_name for Ci::ApplicationRecord
+ model.remove_connection
+ end
+ end
+
+ describe '#read' do
+ it 'returns ci replica connection' do
+ expect { |b| lb.read(&b) }.to yield_with_args do |args|
+ expect(args.pool.db_config.name).to eq('ci_replica')
+ end
+ end
+
+ context 'when GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci=main' do
+ it 'returns ci replica connection' do
+ stub_env('GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci', 'main')
+
+ expect { |b| lb.read(&b) }.to yield_with_args do |args|
+ expect(args.pool.db_config.name).to eq('ci_replica')
+ end
+ end
+ end
+ end
+
+ describe '#read_write' do
+ it 'returns Ci::ApplicationRecord connection' do
+ expect { |b| lb.read_write(&b) }.to yield_with_args do |args|
+ expect(args.pool.db_config.name).to eq('ci')
+ end
+ end
+
+ context 'when GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci=main' do
+ it 'returns ActiveRecord::Base connection' do
+ stub_env('GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci', 'main')
+
+ expect { |b| lb.read_write(&b) }.to yield_with_args do |args|
+ expect(args.pool.db_config.name).to eq('main')
+ end
+ end
+ end
+ end
+ end
+
+ describe '#wal_diff' do
+ it 'returns the diff between two write locations' do
+ loc1 = lb.send(:get_write_location, lb.pool.connection)
+
+ create(:user) # This ensures we get a new WAL location
+
+ loc2 = lb.send(:get_write_location, lb.pool.connection)
+ diff = lb.wal_diff(loc2, loc1)
+
+ expect(diff).to be_positive
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/load_balancing/primary_host_spec.rb b/spec/lib/gitlab/database/load_balancing/primary_host_spec.rb
index 45d81808971..02c9499bedb 100644
--- a/spec/lib/gitlab/database/load_balancing/primary_host_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/primary_host_spec.rb
@@ -51,7 +51,11 @@ RSpec.describe Gitlab::Database::LoadBalancing::PrimaryHost do
end
describe '#offline!' do
- it 'does nothing' do
+ it 'logs the event but does nothing else' do
+ expect(Gitlab::Database::LoadBalancing::Logger).to receive(:warn)
+ .with(hash_including(event: :host_offline))
+ .and_call_original
+
expect(host.offline!).to be_nil
end
end
diff --git a/spec/lib/gitlab/database/load_balancing/rack_middleware_spec.rb b/spec/lib/gitlab/database/load_balancing/rack_middleware_spec.rb
index af7e2a4b167..b768d4ecea3 100644
--- a/spec/lib/gitlab/database/load_balancing/rack_middleware_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/rack_middleware_spec.rb
@@ -6,12 +6,12 @@ RSpec.describe Gitlab::Database::LoadBalancing::RackMiddleware, :redis do
let(:app) { double(:app) }
let(:middleware) { described_class.new(app) }
let(:warden_user) { double(:warden, user: double(:user, id: 42)) }
- let(:single_sticking_object) { Set.new([[ActiveRecord::Base, :user, 42]]) }
+ let(:single_sticking_object) { Set.new([[ActiveRecord::Base.sticking, :user, 42]]) }
let(:multiple_sticking_objects) do
Set.new([
- [ActiveRecord::Base, :user, 42],
- [ActiveRecord::Base, :runner, '123456789'],
- [ActiveRecord::Base, :runner, '1234']
+ [ActiveRecord::Base.sticking, :user, 42],
+ [ActiveRecord::Base.sticking, :runner, '123456789'],
+ [ActiveRecord::Base.sticking, :runner, '1234']
])
end
@@ -162,7 +162,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::RackMiddleware, :redis do
it 'returns the warden user if present' do
env = { 'warden' => warden_user }
ids = Gitlab::Database::LoadBalancing.base_models.map do |model|
- [model, :user, 42]
+ [model.sticking, :user, 42]
end
expect(middleware.sticking_namespaces(env)).to eq(ids)
@@ -181,9 +181,9 @@ RSpec.describe Gitlab::Database::LoadBalancing::RackMiddleware, :redis do
env = { described_class::STICK_OBJECT => multiple_sticking_objects }
expect(middleware.sticking_namespaces(env)).to eq([
- [ActiveRecord::Base, :user, 42],
- [ActiveRecord::Base, :runner, '123456789'],
- [ActiveRecord::Base, :runner, '1234']
+ [ActiveRecord::Base.sticking, :user, 42],
+ [ActiveRecord::Base.sticking, :runner, '123456789'],
+ [ActiveRecord::Base.sticking, :runner, '1234']
])
end
end
diff --git a/spec/lib/gitlab/database/load_balancing/setup_spec.rb b/spec/lib/gitlab/database/load_balancing/setup_spec.rb
index 01646bc76ef..953d83d3b48 100644
--- a/spec/lib/gitlab/database/load_balancing/setup_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/setup_spec.rb
@@ -7,19 +7,20 @@ RSpec.describe Gitlab::Database::LoadBalancing::Setup do
it 'sets up the load balancer' do
setup = described_class.new(ActiveRecord::Base)
- expect(setup).to receive(:disable_prepared_statements)
- expect(setup).to receive(:setup_load_balancer)
+ expect(setup).to receive(:configure_connection)
+ expect(setup).to receive(:setup_connection_proxy)
expect(setup).to receive(:setup_service_discovery)
+ expect(setup).to receive(:setup_feature_flag_to_model_load_balancing)
setup.setup
end
end
- describe '#disable_prepared_statements' do
- it 'disables prepared statements and reconnects to the database' do
+ describe '#configure_connection' do
+ it 'configures pool, prepared statements and reconnects to the database' do
config = double(
:config,
- configuration_hash: { host: 'localhost' },
+ configuration_hash: { host: 'localhost', pool: 2, prepared_statements: true },
env_name: 'test',
name: 'main'
)
@@ -27,7 +28,11 @@ RSpec.describe Gitlab::Database::LoadBalancing::Setup do
expect(ActiveRecord::DatabaseConfigurations::HashConfig)
.to receive(:new)
- .with('test', 'main', { host: 'localhost', prepared_statements: false })
+ .with('test', 'main', {
+ host: 'localhost',
+ prepared_statements: false,
+ pool: Gitlab::Database.default_pool_size
+ })
.and_call_original
# HashConfig doesn't implement its own #==, so we can't directly compare
@@ -36,11 +41,11 @@ RSpec.describe Gitlab::Database::LoadBalancing::Setup do
.to receive(:establish_connection)
.with(an_instance_of(ActiveRecord::DatabaseConfigurations::HashConfig))
- described_class.new(model).disable_prepared_statements
+ described_class.new(model).configure_connection
end
end
- describe '#setup_load_balancer' do
+ describe '#setup_connection_proxy' do
it 'sets up the load balancer' do
model = Class.new(ActiveRecord::Base)
setup = described_class.new(model)
@@ -54,9 +59,9 @@ RSpec.describe Gitlab::Database::LoadBalancing::Setup do
.with(setup.configuration)
.and_return(lb)
- setup.setup_load_balancer
+ setup.setup_connection_proxy
- expect(model.connection.load_balancer).to eq(lb)
+ expect(model.load_balancer).to eq(lb)
expect(model.sticking)
.to be_an_instance_of(Gitlab::Database::LoadBalancing::Sticking)
end
@@ -77,7 +82,6 @@ RSpec.describe Gitlab::Database::LoadBalancing::Setup do
model = ActiveRecord::Base
setup = described_class.new(model)
sv = instance_spy(Gitlab::Database::LoadBalancing::ServiceDiscovery)
- lb = model.connection.load_balancer
allow(setup.configuration)
.to receive(:service_discovery_enabled?)
@@ -85,7 +89,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::Setup do
allow(Gitlab::Database::LoadBalancing::ServiceDiscovery)
.to receive(:new)
- .with(lb, setup.configuration.service_discovery)
+ .with(setup.load_balancer, setup.configuration.service_discovery)
.and_return(sv)
expect(sv).to receive(:perform_service_discovery)
@@ -98,7 +102,6 @@ RSpec.describe Gitlab::Database::LoadBalancing::Setup do
model = ActiveRecord::Base
setup = described_class.new(model, start_service_discovery: true)
sv = instance_spy(Gitlab::Database::LoadBalancing::ServiceDiscovery)
- lb = model.connection.load_balancer
allow(setup.configuration)
.to receive(:service_discovery_enabled?)
@@ -106,7 +109,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::Setup do
allow(Gitlab::Database::LoadBalancing::ServiceDiscovery)
.to receive(:new)
- .with(lb, setup.configuration.service_discovery)
+ .with(setup.load_balancer, setup.configuration.service_discovery)
.and_return(sv)
expect(sv).to receive(:perform_service_discovery)
@@ -116,4 +119,181 @@ RSpec.describe Gitlab::Database::LoadBalancing::Setup do
end
end
end
+
+ describe '#setup_feature_flag_to_model_load_balancing', :reestablished_active_record_base do
+ using RSpec::Parameterized::TableSyntax
+
+ where do
+ {
+ "with model LB enabled it picks a dedicated CI connection" => {
+ env_GITLAB_USE_MODEL_LOAD_BALANCING: 'true',
+ env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: nil,
+ request_store_active: false,
+ ff_use_model_load_balancing: nil,
+ expectations: {
+ main: { read: 'main_replica', write: 'main' },
+ ci: { read: 'ci_replica', write: 'ci' }
+ }
+ },
+ "with model LB enabled and re-use of primary connection it uses CI connection for reads" => {
+ env_GITLAB_USE_MODEL_LOAD_BALANCING: 'true',
+ env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: 'main',
+ request_store_active: false,
+ ff_use_model_load_balancing: nil,
+ expectations: {
+ main: { read: 'main_replica', write: 'main' },
+ ci: { read: 'ci_replica', write: 'main' }
+ }
+ },
+ "with model LB disabled it fallbacks to use main" => {
+ env_GITLAB_USE_MODEL_LOAD_BALANCING: 'false',
+ env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: nil,
+ request_store_active: false,
+ ff_use_model_load_balancing: nil,
+ expectations: {
+ main: { read: 'main_replica', write: 'main' },
+ ci: { read: 'main_replica', write: 'main' }
+ }
+ },
+ "with model LB disabled, but re-use configured it fallbacks to use main" => {
+ env_GITLAB_USE_MODEL_LOAD_BALANCING: 'false',
+ env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: 'main',
+ request_store_active: false,
+ ff_use_model_load_balancing: nil,
+ expectations: {
+ main: { read: 'main_replica', write: 'main' },
+ ci: { read: 'main_replica', write: 'main' }
+ }
+ },
+ "with FF disabled without RequestStore it uses main" => {
+ env_GITLAB_USE_MODEL_LOAD_BALANCING: nil,
+ env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: nil,
+ request_store_active: false,
+ ff_use_model_load_balancing: false,
+ expectations: {
+ main: { read: 'main_replica', write: 'main' },
+ ci: { read: 'main_replica', write: 'main' }
+ }
+ },
+ "with FF enabled without RequestStore sticking of FF does not work, so it fallbacks to use main" => {
+ env_GITLAB_USE_MODEL_LOAD_BALANCING: nil,
+ env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: nil,
+ request_store_active: false,
+ ff_use_model_load_balancing: true,
+ expectations: {
+ main: { read: 'main_replica', write: 'main' },
+ ci: { read: 'main_replica', write: 'main' }
+ }
+ },
+ "with FF disabled with RequestStore it uses main" => {
+ env_GITLAB_USE_MODEL_LOAD_BALANCING: nil,
+ env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: nil,
+ request_store_active: true,
+ ff_use_model_load_balancing: false,
+ expectations: {
+ main: { read: 'main_replica', write: 'main' },
+ ci: { read: 'main_replica', write: 'main' }
+ }
+ },
+ "with FF enabled with RequestStore it sticks FF and uses CI connection" => {
+ env_GITLAB_USE_MODEL_LOAD_BALANCING: nil,
+ env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: nil,
+ request_store_active: true,
+ ff_use_model_load_balancing: true,
+ expectations: {
+ main: { read: 'main_replica', write: 'main' },
+ ci: { read: 'ci_replica', write: 'ci' }
+ }
+ },
+ "with re-use and FF enabled with RequestStore it sticks FF and uses CI connection for reads" => {
+ env_GITLAB_USE_MODEL_LOAD_BALANCING: nil,
+ env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci: 'main',
+ request_store_active: true,
+ ff_use_model_load_balancing: true,
+ expectations: {
+ main: { read: 'main_replica', write: 'main' },
+ ci: { read: 'ci_replica', write: 'main' }
+ }
+ }
+ }
+ end
+
+ with_them do
+ let(:ci_class) do
+ Class.new(ActiveRecord::Base) do
+ def self.name
+ 'Ci::ApplicationRecordTemporary'
+ end
+
+ establish_connection ActiveRecord::DatabaseConfigurations::HashConfig.new(
+ Rails.env,
+ 'ci',
+ ActiveRecord::Base.connection_db_config.configuration_hash
+ )
+ end
+ end
+
+ let(:models) do
+ {
+ main: ActiveRecord::Base,
+ ci: ci_class
+ }
+ end
+
+ around do |example|
+ if request_store_active
+ Gitlab::WithRequestStore.with_request_store do
+ RequestStore.clear!
+
+ example.run
+ end
+ else
+ example.run
+ end
+ end
+
+ before do
+ # Rewrite `class_attribute` to use rspec mocking and prevent modifying the objects
+ allow_next_instance_of(described_class) do |setup|
+ allow(setup).to receive(:configure_connection)
+
+ allow(setup).to receive(:setup_class_attribute) do |attribute, value|
+ allow(setup.model).to receive(attribute) { value }
+ end
+ end
+
+ stub_env('GITLAB_USE_MODEL_LOAD_BALANCING', env_GITLAB_USE_MODEL_LOAD_BALANCING)
+ stub_env('GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci', env_GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci)
+ stub_feature_flags(use_model_load_balancing: ff_use_model_load_balancing)
+
+ # Make load balancer to force init with a dedicated replicas connections
+ models.each do |_, model|
+ described_class.new(model).tap do |subject|
+ subject.configuration.hosts = [subject.configuration.replica_db_config.host]
+ subject.setup
+ end
+ end
+ end
+
+ it 'results match expectations' do
+ result = models.transform_values do |model|
+ load_balancer = model.connection.instance_variable_get(:@load_balancer)
+
+ {
+ read: load_balancer.read { |connection| connection.pool.db_config.name },
+ write: load_balancer.read_write { |connection| connection.pool.db_config.name }
+ }
+ end
+
+ expect(result).to eq(expectations)
+ end
+
+ it 'does return load_balancer assigned to a given connection' do
+ models.each do |name, model|
+ expect(model.load_balancer.name).to eq(name)
+ expect(model.sticking.instance_variable_get(:@load_balancer)).to eq(model.load_balancer)
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/load_balancing/sidekiq_client_middleware_spec.rb b/spec/lib/gitlab/database/load_balancing/sidekiq_client_middleware_spec.rb
index 08dd6a0a788..9acf80e684f 100644
--- a/spec/lib/gitlab/database/load_balancing/sidekiq_client_middleware_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/sidekiq_client_middleware_spec.rb
@@ -181,11 +181,11 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqClientMiddleware do
end
context 'when worker data consistency is :delayed' do
- include_examples 'mark data consistency location', :delayed
+ include_examples 'mark data consistency location', :delayed
end
context 'when worker data consistency is :sticky' do
- include_examples 'mark data consistency location', :sticky
+ include_examples 'mark data consistency location', :sticky
end
end
end
diff --git a/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb b/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb
index 06efdcd8f99..de2ad662d16 100644
--- a/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb
@@ -64,7 +64,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware, :clean_
let(:wal_locations) { { Gitlab::Database::MAIN_DATABASE_NAME.to_sym => location } }
it 'does not stick to the primary', :aggregate_failures do
- expect(ActiveRecord::Base.connection.load_balancer)
+ expect(ActiveRecord::Base.load_balancer)
.to receive(:select_up_to_date_host)
.with(location)
.and_return(true)
@@ -107,7 +107,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware, :clean_
let(:job) { { 'job_id' => 'a180b47c-3fd6-41b8-81e9-34da61c3400e', 'dedup_wal_locations' => wal_locations } }
before do
- allow(ActiveRecord::Base.connection.load_balancer)
+ allow(ActiveRecord::Base.load_balancer)
.to receive(:select_up_to_date_host)
.with(wal_locations[:main])
.and_return(true)
@@ -120,7 +120,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware, :clean_
let(:job) { { 'job_id' => 'a180b47c-3fd6-41b8-81e9-34da61c3400e', 'database_write_location' => '0/D525E3A8' } }
before do
- allow(ActiveRecord::Base.connection.load_balancer)
+ allow(ActiveRecord::Base.load_balancer)
.to receive(:select_up_to_date_host)
.with('0/D525E3A8')
.and_return(true)
diff --git a/spec/lib/gitlab/database/load_balancing/sticking_spec.rb b/spec/lib/gitlab/database/load_balancing/sticking_spec.rb
index 8ceda52ee85..d88554614cf 100644
--- a/spec/lib/gitlab/database/load_balancing/sticking_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/sticking_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Database::LoadBalancing::Sticking, :redis do
let(:sticking) do
- described_class.new(ActiveRecord::Base.connection.load_balancer)
+ described_class.new(ActiveRecord::Base.load_balancer)
end
after do
@@ -22,7 +22,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::Sticking, :redis do
sticking.stick_or_unstick_request(env, :user, 42)
expect(env[Gitlab::Database::LoadBalancing::RackMiddleware::STICK_OBJECT].to_a)
- .to eq([[ActiveRecord::Base, :user, 42]])
+ .to eq([[sticking, :user, 42]])
end
it 'sticks or unsticks multiple objects and updates the Rack environment' do
@@ -42,8 +42,8 @@ RSpec.describe Gitlab::Database::LoadBalancing::Sticking, :redis do
sticking.stick_or_unstick_request(env, :runner, '123456789')
expect(env[Gitlab::Database::LoadBalancing::RackMiddleware::STICK_OBJECT].to_a).to eq([
- [ActiveRecord::Base, :user, 42],
- [ActiveRecord::Base, :runner, '123456789']
+ [sticking, :user, 42],
+ [sticking, :runner, '123456789']
])
end
end
@@ -73,7 +73,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::Sticking, :redis do
end
describe '#all_caught_up?' do
- let(:lb) { ActiveRecord::Base.connection.load_balancer }
+ let(:lb) { ActiveRecord::Base.load_balancer }
let(:last_write_location) { 'foo' }
before do
@@ -137,7 +137,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::Sticking, :redis do
end
describe '#unstick_or_continue_sticking' do
- let(:lb) { ActiveRecord::Base.connection.load_balancer }
+ let(:lb) { ActiveRecord::Base.load_balancer }
it 'simply returns if no write location could be found' do
allow(sticking)
@@ -182,13 +182,13 @@ RSpec.describe Gitlab::Database::LoadBalancing::Sticking, :redis do
RSpec.shared_examples 'sticking' do
before do
- allow(ActiveRecord::Base.connection.load_balancer)
+ allow(ActiveRecord::Base.load_balancer)
.to receive(:primary_write_location)
.and_return('foo')
end
it 'sticks an entity to the primary', :aggregate_failures do
- allow(ActiveRecord::Base.connection.load_balancer)
+ allow(ActiveRecord::Base.load_balancer)
.to receive(:primary_only?)
.and_return(false)
@@ -227,11 +227,11 @@ RSpec.describe Gitlab::Database::LoadBalancing::Sticking, :redis do
describe '#mark_primary_write_location' do
it 'updates the write location with the load balancer' do
- allow(ActiveRecord::Base.connection.load_balancer)
+ allow(ActiveRecord::Base.load_balancer)
.to receive(:primary_write_location)
.and_return('foo')
- allow(ActiveRecord::Base.connection.load_balancer)
+ allow(ActiveRecord::Base.load_balancer)
.to receive(:primary_only?)
.and_return(false)
@@ -291,7 +291,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::Sticking, :redis do
end
describe '#select_caught_up_replicas' do
- let(:lb) { ActiveRecord::Base.connection.load_balancer }
+ let(:lb) { ActiveRecord::Base.load_balancer }
context 'with no write location' do
before do
diff --git a/spec/lib/gitlab/database/load_balancing_spec.rb b/spec/lib/gitlab/database/load_balancing_spec.rb
index bf5314e2c34..65ffe539910 100644
--- a/spec/lib/gitlab/database/load_balancing_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Gitlab::Database::LoadBalancing do
expect(models).to include(ActiveRecord::Base)
if Gitlab::Database.has_config?(:ci)
- expect(models).to include(Ci::CiDatabaseRecord)
+ expect(models).to include(Ci::ApplicationRecord)
end
end
@@ -76,7 +76,7 @@ RSpec.describe Gitlab::Database::LoadBalancing do
context 'when a read connection is used' do
it 'returns :replica' do
- proxy.load_balancer.read do |connection|
+ load_balancer.read do |connection|
expect(described_class.db_role_for_connection(connection)).to eq(:replica)
end
end
@@ -84,7 +84,7 @@ RSpec.describe Gitlab::Database::LoadBalancing do
context 'when a read_write connection is used' do
it 'returns :primary' do
- proxy.load_balancer.read_write do |connection|
+ load_balancer.read_write do |connection|
expect(described_class.db_role_for_connection(connection)).to eq(:primary)
end
end
@@ -105,7 +105,7 @@ RSpec.describe Gitlab::Database::LoadBalancing do
describe 'LoadBalancing integration tests', :database_replica, :delete do
before(:all) do
ActiveRecord::Schema.define do
- create_table :load_balancing_test, force: true do |t|
+ create_table :_test_load_balancing_test, force: true do |t|
t.string :name, null: true
end
end
@@ -113,13 +113,13 @@ RSpec.describe Gitlab::Database::LoadBalancing do
after(:all) do
ActiveRecord::Schema.define do
- drop_table :load_balancing_test, force: true
+ drop_table :_test_load_balancing_test, force: true
end
end
let(:model) do
Class.new(ApplicationRecord) do
- self.table_name = "load_balancing_test"
+ self.table_name = "_test_load_balancing_test"
end
end
@@ -443,7 +443,7 @@ RSpec.describe Gitlab::Database::LoadBalancing do
elsif payload[:name] == 'SQL' # Custom query
true
else
- keywords = %w[load_balancing_test]
+ keywords = %w[_test_load_balancing_test]
keywords += %w[begin commit] if include_transaction
keywords.any? { |keyword| payload[:sql].downcase.include?(keyword) }
end
diff --git a/spec/lib/gitlab/database/migration_helpers/loose_foreign_key_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers/loose_foreign_key_helpers_spec.rb
index 54b3ad22faf..f1dbfbbff18 100644
--- a/spec/lib/gitlab/database/migration_helpers/loose_foreign_key_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers/loose_foreign_key_helpers_spec.rb
@@ -9,18 +9,18 @@ RSpec.describe Gitlab::Database::MigrationHelpers::LooseForeignKeyHelpers do
let(:model) do
Class.new(ApplicationRecord) do
- self.table_name = 'loose_fk_test_table'
+ self.table_name = '_test_loose_fk_test_table'
end
end
before(:all) do
- migration.create_table :loose_fk_test_table do |t|
+ migration.create_table :_test_loose_fk_test_table do |t|
t.timestamps
end
end
after(:all) do
- migration.drop_table :loose_fk_test_table
+ migration.drop_table :_test_loose_fk_test_table
end
before do
@@ -37,7 +37,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::LooseForeignKeyHelpers do
context 'when the record deletion tracker trigger is installed' do
before do
- migration.track_record_deletions(:loose_fk_test_table)
+ migration.track_record_deletions(:_test_loose_fk_test_table)
end
it 'stores the record deletion' do
@@ -50,7 +50,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::LooseForeignKeyHelpers do
deleted_record = LooseForeignKeys::DeletedRecord.all.first
expect(deleted_record.primary_key_value).to eq(record_to_be_deleted.id)
- expect(deleted_record.fully_qualified_table_name).to eq('public.loose_fk_test_table')
+ expect(deleted_record.fully_qualified_table_name).to eq('public._test_loose_fk_test_table')
expect(deleted_record.partition).to eq(1)
end
diff --git a/spec/lib/gitlab/database/migration_helpers/v2_spec.rb b/spec/lib/gitlab/database/migration_helpers/v2_spec.rb
index 854e97ef897..acf775b3538 100644
--- a/spec/lib/gitlab/database/migration_helpers/v2_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers/v2_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::V2 do
let(:model) { Class.new(ActiveRecord::Base) }
before do
- model.table_name = :test_table
+ model.table_name = :_test_table
end
context 'when called inside a transaction block' do
@@ -30,19 +30,19 @@ RSpec.describe Gitlab::Database::MigrationHelpers::V2 do
it 'raises an error' do
expect do
- migration.public_send(operation, :test_table, :original, :renamed)
+ 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'
+ 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)
+ migration.public_send(operation, :_test_table, :original, :renamed)
end.to raise_error("#{operation} does not currently support columns with default values")
end
end
@@ -51,18 +51,18 @@ RSpec.describe Gitlab::Database::MigrationHelpers::V2 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')
+ 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)
+ .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)
+ migration.public_send(operation, :_test_table, :original, :renamed, batch_column_name: :status)
end
end
end
@@ -71,17 +71,17 @@ RSpec.describe Gitlab::Database::MigrationHelpers::V2 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)
+ migration.send(operation, :_test_table, :original, :renamed)
model.reset_column_information
- expect(migration.column_exists?(:test_table, added_column)).to eq(true)
+ 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)
+ migration.public_send(operation, :_test_table, :original, :renamed)
model.reset_column_information
new_record_1 = model.create!(status: 1, original: 'first')
@@ -102,7 +102,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::V2 do
before do
allow(migration).to receive(:transaction_open?).and_return(false)
- migration.create_table :test_table do |t|
+ migration.create_table :_test_table do |t|
t.integer :status, null: false
t.text :original
t.text :other_column
@@ -118,8 +118,8 @@ RSpec.describe Gitlab::Database::MigrationHelpers::V2 do
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')
+ 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
@@ -128,7 +128,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::V2 do
before do
allow(migration).to receive(:transaction_open?).and_return(false)
- migration.create_table :test_table do |t|
+ migration.create_table :_test_table do |t|
t.integer :status, null: false
t.text :other_column
t.text :renamed
@@ -144,8 +144,8 @@ RSpec.describe Gitlab::Database::MigrationHelpers::V2 do
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')
+ 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
@@ -156,25 +156,25 @@ RSpec.describe Gitlab::Database::MigrationHelpers::V2 do
before do
allow(migration).to receive(:transaction_open?).and_return(false)
- migration.create_table :test_table do |t|
+ 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
+ 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)
+ 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)
+ migration.public_send(operation, :_test_table, :original, :renamed)
end.not_to raise_error
end
end
@@ -182,26 +182,26 @@ RSpec.describe Gitlab::Database::MigrationHelpers::V2 do
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']
+ ['trigger_020dbcb8cdd0', 'function_for_trigger_020dbcb8cdd0', before: 'insert'],
+ ['trigger_6edaca641d03', 'function_for_trigger_6edaca641d03', before: 'update'],
+ ['trigger_a3fb9f3add34', 'function_for_trigger_a3fb9f3add34', 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)
+ expect_valid_function_trigger(:_test_table, trigger_name, function_name, event)
end
- expect(migration.column_exists?(:test_table, added_column)).to eq(true)
+ expect(migration.column_exists?(:_test_table, added_column)).to eq(true)
- migration.public_send(operation, :test_table, :original, :renamed)
+ migration.public_send(operation, :_test_table, :original, :renamed)
- expect(migration.column_exists?(:test_table, added_column)).to eq(false)
+ 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_trigger_not_to_exist(:_test_table, trigger_name)
expect_function_not_to_exist(function_name)
end
end
@@ -223,7 +223,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::V2 do
end
describe '#create_table' do
- let(:table_name) { :test_table }
+ let(:table_name) { :_test_table }
let(:column_attributes) do
[
{ name: 'id', sql_type: 'bigint', null: false, default: nil },
@@ -245,7 +245,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::V2 do
end
expect_table_columns_to_match(column_attributes, table_name)
- expect_check_constraint(table_name, 'check_cda6f69506', 'char_length(name) <= 100')
+ expect_check_constraint(table_name, 'check_e9982cf9da', 'char_length(name) <= 100')
end
end
end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index d89af1521a2..ea755f5a368 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -31,16 +31,10 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
end
describe '#add_timestamps_with_timezone' do
- let(:in_transaction) { false }
-
- before do
- allow(model).to receive(:transaction_open?).and_return(in_transaction)
- allow(model).to receive(:disable_statement_timeout)
- end
-
it 'adds "created_at" and "updated_at" fields with the "datetime_with_timezone" data type' do
Gitlab::Database::MigrationHelpers::DEFAULT_TIMESTAMP_COLUMNS.each do |column_name|
- expect(model).to receive(:add_column).with(:foo, column_name, :datetime_with_timezone, { null: false })
+ expect(model).to receive(:add_column)
+ .with(:foo, column_name, :datetime_with_timezone, { default: nil, null: false })
end
model.add_timestamps_with_timezone(:foo)
@@ -48,7 +42,8 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
it 'can disable the NOT NULL constraint' do
Gitlab::Database::MigrationHelpers::DEFAULT_TIMESTAMP_COLUMNS.each do |column_name|
- expect(model).to receive(:add_column).with(:foo, column_name, :datetime_with_timezone, { null: true })
+ expect(model).to receive(:add_column)
+ .with(:foo, column_name, :datetime_with_timezone, { default: nil, null: true })
end
model.add_timestamps_with_timezone(:foo, null: true)
@@ -64,9 +59,10 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
it 'can add choice of acceptable columns' do
expect(model).to receive(:add_column).with(:foo, :created_at, :datetime_with_timezone, anything)
expect(model).to receive(:add_column).with(:foo, :deleted_at, :datetime_with_timezone, anything)
+ expect(model).to receive(:add_column).with(:foo, :processed_at, :datetime_with_timezone, anything)
expect(model).not_to receive(:add_column).with(:foo, :updated_at, :datetime_with_timezone, anything)
- model.add_timestamps_with_timezone(:foo, columns: [:created_at, :deleted_at])
+ model.add_timestamps_with_timezone(:foo, columns: [:created_at, :deleted_at, :processed_at])
end
it 'cannot add unacceptable column names' do
@@ -74,29 +70,6 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
model.add_timestamps_with_timezone(:foo, columns: [:bar])
end.to raise_error %r/Illegal timestamp column name/
end
-
- context 'in a transaction' do
- let(:in_transaction) { true }
-
- before do
- allow(model).to receive(:add_column).with(any_args).and_call_original
- allow(model).to receive(:add_column)
- .with(:foo, anything, :datetime_with_timezone, anything)
- .and_return(nil)
- end
-
- it 'cannot add a default value' do
- expect do
- model.add_timestamps_with_timezone(:foo, default: :i_cause_an_error)
- end.to raise_error %r/add_timestamps_with_timezone/
- end
-
- it 'can add columns without defaults' do
- expect do
- model.add_timestamps_with_timezone(:foo)
- end.not_to raise_error
- end
- end
end
describe '#create_table_with_constraints' do
@@ -271,12 +244,92 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
model.add_concurrent_index(:users, :foo, unique: true)
end
- it 'does nothing if the index exists already' do
- expect(model).to receive(:index_exists?)
- .with(:users, :foo, { algorithm: :concurrently, unique: true }).and_return(true)
- expect(model).not_to receive(:add_index)
+ context 'when the index exists and is valid' do
+ before do
+ model.add_index :users, :id, unique: true
+ end
- model.add_concurrent_index(:users, :foo, unique: true)
+ it 'does leaves the existing index' do
+ expect(model).to receive(:index_exists?)
+ .with(:users, :id, { algorithm: :concurrently, unique: true }).and_call_original
+
+ expect(model).not_to receive(:remove_index)
+ expect(model).not_to receive(:add_index)
+
+ model.add_concurrent_index(:users, :id, unique: true)
+ end
+ end
+
+ context 'when an invalid copy of the index exists' do
+ before do
+ model.add_index :users, :id, unique: true, name: index_name
+
+ model.connection.execute(<<~SQL)
+ UPDATE pg_index
+ SET indisvalid = false
+ WHERE indexrelid = '#{index_name}'::regclass
+ SQL
+ end
+
+ context 'when the default name is used' do
+ let(:index_name) { model.index_name(:users, :id) }
+
+ it 'drops and recreates the index' do
+ expect(model).to receive(:index_exists?)
+ .with(:users, :id, { algorithm: :concurrently, unique: true }).and_call_original
+ expect(model).to receive(:index_invalid?).with(index_name, schema: nil).and_call_original
+
+ expect(model).to receive(:remove_concurrent_index_by_name).with(:users, index_name)
+
+ expect(model).to receive(:add_index)
+ .with(:users, :id, { algorithm: :concurrently, unique: true })
+
+ model.add_concurrent_index(:users, :id, unique: true)
+ end
+ end
+
+ context 'when a custom name is used' do
+ let(:index_name) { 'my_test_index' }
+
+ it 'drops and recreates the index' do
+ expect(model).to receive(:index_exists?)
+ .with(:users, :id, { algorithm: :concurrently, unique: true, name: index_name }).and_call_original
+ expect(model).to receive(:index_invalid?).with(index_name, schema: nil).and_call_original
+
+ expect(model).to receive(:remove_concurrent_index_by_name).with(:users, index_name)
+
+ expect(model).to receive(:add_index)
+ .with(:users, :id, { algorithm: :concurrently, unique: true, name: index_name })
+
+ model.add_concurrent_index(:users, :id, unique: true, name: index_name)
+ end
+ end
+
+ context 'when a qualified table name is used' do
+ let(:other_schema) { 'foo_schema' }
+ let(:index_name) { 'my_test_index' }
+ let(:table_name) { "#{other_schema}.users" }
+
+ before do
+ model.connection.execute(<<~SQL)
+ CREATE SCHEMA #{other_schema};
+ ALTER TABLE users SET SCHEMA #{other_schema};
+ SQL
+ end
+
+ it 'drops and recreates the index' do
+ expect(model).to receive(:index_exists?)
+ .with(table_name, :id, { algorithm: :concurrently, unique: true, name: index_name }).and_call_original
+ expect(model).to receive(:index_invalid?).with(index_name, schema: other_schema).and_call_original
+
+ expect(model).to receive(:remove_concurrent_index_by_name).with(table_name, index_name)
+
+ expect(model).to receive(:add_index)
+ .with(table_name, :id, { algorithm: :concurrently, unique: true, name: index_name })
+
+ model.add_concurrent_index(table_name, :id, unique: true, name: index_name)
+ end
+ end
end
it 'unprepares the async index creation' do
diff --git a/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb b/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb
index 1a7116e75e5..e42a6c970ea 100644
--- a/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb
@@ -583,12 +583,33 @@ RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do
end
describe '#finalized_background_migration' do
- include_context 'background migration job class'
+ let(:job_coordinator) { Gitlab::BackgroundMigration::JobCoordinator.new(:main, BackgroundMigrationWorker) }
+
+ let!(:job_class_name) { 'TestJob' }
+ let!(:job_class) { Class.new }
+ let!(:job_perform_method) do
+ ->(*arguments) do
+ Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
+ # Value is 'TestJob' defined by :job_class_name in the let! above.
+ # Scoping prohibits us from directly referencing job_class_name.
+ RSpec.current_example.example_group_instance.job_class_name,
+ arguments
+ )
+ end
+ end
let!(:tracked_pending_job) { create(:background_migration_job, class_name: job_class_name, status: :pending, arguments: [1]) }
let!(:tracked_successful_job) { create(:background_migration_job, class_name: job_class_name, status: :succeeded, arguments: [2]) }
before do
+ job_class.define_method(:perform, job_perform_method)
+
+ allow(Gitlab::BackgroundMigration).to receive(:coordinator_for_database)
+ .with(:main).and_return(job_coordinator)
+
+ expect(job_coordinator).to receive(:migration_class_for)
+ .with(job_class_name).at_least(:once) { job_class }
+
Sidekiq::Testing.disable! do
BackgroundMigrationWorker.perform_async(job_class_name, [1, 2])
BackgroundMigrationWorker.perform_async(job_class_name, [3, 4])
diff --git a/spec/lib/gitlab/database/migrations/observers/transaction_duration_spec.rb b/spec/lib/gitlab/database/migrations/observers/transaction_duration_spec.rb
new file mode 100644
index 00000000000..e65f89747c4
--- /dev/null
+++ b/spec/lib/gitlab/database/migrations/observers/transaction_duration_spec.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::Migrations::Observers::TransactionDuration do
+ subject(:transaction_duration_observer) { described_class.new(observation, directory_path) }
+
+ let(:observation) { Gitlab::Database::Migrations::Observation.new(migration_version, migration_name) }
+ let(:directory_path) { Dir.mktmpdir }
+ let(:log_file) { "#{directory_path}/#{migration_version}_#{migration_name}-transaction-duration.json" }
+ let(:transaction_duration) { Gitlab::Json.parse(File.read(log_file)) }
+ let(:migration_version) { 20210422152437 }
+ let(:migration_name) { 'test' }
+
+ after do
+ FileUtils.remove_entry(directory_path)
+ end
+
+ it 'records real and sub transactions duration', :delete do
+ observe
+
+ entry = transaction_duration[0]
+ start_time, end_time, transaction_type = entry.values_at('start_time', 'end_time', 'transaction_type')
+ start_time = DateTime.parse(start_time)
+ end_time = DateTime.parse(end_time)
+
+ aggregate_failures do
+ expect(transaction_duration.size).to eq(3)
+ expect(start_time).to be_before(end_time)
+ expect(transaction_type).not_to be_nil
+ end
+ end
+
+ context 'when there are sub-transactions' do
+ it 'records transaction duration' do
+ observe_sub_transaction
+
+ expect(transaction_duration.size).to eq(1)
+
+ entry = transaction_duration[0]['transaction_type']
+
+ expect(entry).to eql 'sub_transaction'
+ end
+ end
+
+ context 'when there are real-transactions' do
+ it 'records transaction duration', :delete do
+ observe_real_transaction
+
+ expect(transaction_duration.size).to eq(1)
+
+ entry = transaction_duration[0]['transaction_type']
+
+ expect(entry).to eql 'real_transaction'
+ end
+ end
+
+ private
+
+ def observe
+ transaction_duration_observer.before
+ run_transaction
+ transaction_duration_observer.after
+ transaction_duration_observer.record
+ end
+
+ def observe_sub_transaction
+ transaction_duration_observer.before
+ run_sub_transactions
+ transaction_duration_observer.after
+ transaction_duration_observer.record
+ end
+
+ def observe_real_transaction
+ transaction_duration_observer.before
+ run_real_transactions
+ transaction_duration_observer.after
+ transaction_duration_observer.record
+ end
+
+ def run_real_transactions
+ ActiveRecord::Base.transaction do
+ end
+ end
+
+ def run_sub_transactions
+ ActiveRecord::Base.transaction(requires_new: true) do
+ end
+ end
+
+ def run_transaction
+ ActiveRecord::Base.connection_pool.with_connection do |connection|
+ Gitlab::Database::SharedModel.using_connection(connection) do
+ Gitlab::Database::SharedModel.transaction do
+ Gitlab::Database::SharedModel.transaction(requires_new: true) do
+ Gitlab::Database::SharedModel.transaction do
+ Gitlab::Database::SharedModel.transaction do
+ Gitlab::Database::SharedModel.transaction(requires_new: true) do
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb b/spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb
index 8c406c90e36..b2c4e4b54a4 100644
--- a/spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb
+++ b/spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb
@@ -5,6 +5,8 @@ require 'spec_helper'
RSpec.describe Gitlab::Database::Partitioning::DetachedPartitionDropper do
include Database::TableSchemaHelpers
+ subject(:dropper) { described_class.new }
+
let(:connection) { ActiveRecord::Base.connection }
def expect_partition_present(name)
@@ -23,10 +25,18 @@ RSpec.describe Gitlab::Database::Partitioning::DetachedPartitionDropper do
before do
connection.execute(<<~SQL)
+ CREATE TABLE referenced_table (
+ id bigserial primary key not null
+ )
+ SQL
+ connection.execute(<<~SQL)
+
CREATE TABLE parent_table (
id bigserial not null,
+ referenced_id bigint not null,
created_at timestamptz not null,
- primary key (id, created_at)
+ primary key (id, created_at),
+ constraint fk_referenced foreign key (referenced_id) references referenced_table(id)
) PARTITION BY RANGE(created_at)
SQL
end
@@ -59,7 +69,7 @@ RSpec.describe Gitlab::Database::Partitioning::DetachedPartitionDropper do
attached: false,
drop_after: 1.day.from_now)
- subject.perform
+ dropper.perform
expect_partition_present('test_partition')
end
@@ -75,7 +85,7 @@ RSpec.describe Gitlab::Database::Partitioning::DetachedPartitionDropper do
end
it 'drops the partition' do
- subject.perform
+ dropper.perform
expect(table_oid('test_partition')).to be_nil
end
@@ -86,16 +96,62 @@ RSpec.describe Gitlab::Database::Partitioning::DetachedPartitionDropper do
end
it 'does not drop the partition' do
- subject.perform
+ dropper.perform
expect(table_oid('test_partition')).not_to be_nil
end
end
+ context 'removing foreign keys' do
+ it 'removes foreign keys from the table before dropping it' do
+ expect(dropper).to receive(:drop_detached_partition).and_wrap_original do |drop_method, partition_name|
+ expect(partition_name).to eq('test_partition')
+ expect(foreign_key_exists_by_name(partition_name, 'fk_referenced', schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA)).to be_falsey
+
+ drop_method.call(partition_name)
+ end
+
+ expect(foreign_key_exists_by_name('test_partition', 'fk_referenced', schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA)).to be_truthy
+
+ dropper.perform
+ end
+
+ it 'does not remove foreign keys from the parent table' do
+ expect { dropper.perform }.not_to change { foreign_key_exists_by_name('parent_table', 'fk_referenced') }.from(true)
+ end
+
+ context 'when another process drops the foreign key' do
+ it 'skips dropping that foreign key' do
+ expect(dropper).to receive(:drop_foreign_key_if_present).and_wrap_original do |drop_meth, *args|
+ connection.execute('alter table gitlab_partitions_dynamic.test_partition drop constraint fk_referenced;')
+ drop_meth.call(*args)
+ end
+
+ dropper.perform
+
+ expect_partition_removed('test_partition')
+ end
+ end
+
+ context 'when another process drops the partition' do
+ it 'skips dropping the foreign key' do
+ expect(dropper).to receive(:drop_foreign_key_if_present).and_wrap_original do |drop_meth, *args|
+ connection.execute('drop table gitlab_partitions_dynamic.test_partition')
+ Postgresql::DetachedPartition.where(table_name: 'test_partition').delete_all
+ end
+
+ expect(Gitlab::AppLogger).not_to receive(:error)
+ dropper.perform
+ end
+ end
+ end
+
context 'when another process drops the table while the first waits for a lock' do
it 'skips the table' do
+ # First call to .lock is for removing foreign keys
+ expect(Postgresql::DetachedPartition).to receive(:lock).once.ordered.and_call_original
# Rspec's receive_method_chain does not support .and_wrap_original, so we need to nest here.
- expect(Postgresql::DetachedPartition).to receive(:lock).and_wrap_original do |lock_meth|
+ expect(Postgresql::DetachedPartition).to receive(:lock).once.ordered.and_wrap_original do |lock_meth|
locked = lock_meth.call
expect(locked).to receive(:find_by).and_wrap_original do |find_meth, *find_args|
# Another process drops the table then deletes this entry
@@ -106,9 +162,9 @@ RSpec.describe Gitlab::Database::Partitioning::DetachedPartitionDropper do
locked
end
- expect(subject).not_to receive(:drop_one)
+ expect(dropper).not_to receive(:drop_one)
- subject.perform
+ dropper.perform
end
end
end
@@ -123,19 +179,26 @@ RSpec.describe Gitlab::Database::Partitioning::DetachedPartitionDropper do
end
it 'does not drop the partition, but does remove the DetachedPartition entry' do
- subject.perform
+ dropper.perform
aggregate_failures do
expect(table_oid('test_partition')).not_to be_nil
expect(Postgresql::DetachedPartition.find_by(table_name: 'test_partition')).to be_nil
end
end
- it 'removes the detached_partition entry' do
- detached_partition = Postgresql::DetachedPartition.find_by!(table_name: 'test_partition')
+ context 'when another process removes the entry before this process' do
+ it 'does nothing' do
+ expect(Postgresql::DetachedPartition).to receive(:lock).and_wrap_original do |lock_meth|
+ Postgresql::DetachedPartition.delete_all
+ lock_meth.call
+ end
- subject.perform
+ expect(Gitlab::AppLogger).not_to receive(:error)
- expect(Postgresql::DetachedPartition.exists?(id: detached_partition.id)).to be_falsey
+ dropper.perform
+
+ expect(table_oid('test_partition')).not_to be_nil
+ end
end
end
@@ -155,7 +218,7 @@ RSpec.describe Gitlab::Database::Partitioning::DetachedPartitionDropper do
end
it 'drops both partitions' do
- subject.perform
+ dropper.perform
expect_partition_removed('partition_1')
expect_partition_removed('partition_2')
@@ -163,10 +226,10 @@ RSpec.describe Gitlab::Database::Partitioning::DetachedPartitionDropper do
context 'when the first drop returns an error' do
it 'still drops the second partition' do
- expect(subject).to receive(:drop_detached_partition).ordered.and_raise('injected error')
- expect(subject).to receive(:drop_detached_partition).ordered.and_call_original
+ expect(dropper).to receive(:drop_detached_partition).ordered.and_raise('injected error')
+ expect(dropper).to receive(:drop_detached_partition).ordered.and_call_original
- subject.perform
+ dropper.perform
# We don't know which partition we tried to drop first, so the tests here have to work with either one
expect(Postgresql::DetachedPartition.count).to eq(1)
diff --git a/spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb b/spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb
index 27ada12b067..67d80d71e2a 100644
--- a/spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb
+++ b/spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy do
let(:model) { double('model', table_name: table_name) }
let(:partitioning_key) { double }
- let(:table_name) { :partitioned_test }
+ let(:table_name) { :_test_partitioned_test }
before do
connection.execute(<<~SQL)
@@ -18,11 +18,11 @@ RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy do
(id serial not null, created_at timestamptz not null, PRIMARY KEY (id, created_at))
PARTITION BY RANGE (created_at);
- CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.partitioned_test_000000
+ CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_partitioned_test_000000
PARTITION OF #{table_name}
FOR VALUES FROM (MINVALUE) TO ('2020-05-01');
- CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.partitioned_test_202005
+ CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_partitioned_test_202005
PARTITION OF #{table_name}
FOR VALUES FROM ('2020-05-01') TO ('2020-06-01');
SQL
@@ -30,8 +30,8 @@ RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy do
it 'detects both partitions' do
expect(subject).to eq([
- Gitlab::Database::Partitioning::TimePartition.new(table_name, nil, '2020-05-01', partition_name: 'partitioned_test_000000'),
- Gitlab::Database::Partitioning::TimePartition.new(table_name, '2020-05-01', '2020-06-01', partition_name: 'partitioned_test_202005')
+ Gitlab::Database::Partitioning::TimePartition.new(table_name, nil, '2020-05-01', partition_name: '_test_partitioned_test_000000'),
+ Gitlab::Database::Partitioning::TimePartition.new(table_name, '2020-05-01', '2020-06-01', partition_name: '_test_partitioned_test_202005')
])
end
end
@@ -41,7 +41,7 @@ RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy do
let(:model) do
Class.new(ActiveRecord::Base) do
- self.table_name = 'partitioned_test'
+ self.table_name = '_test_partitioned_test'
self.primary_key = :id
end
end
@@ -59,11 +59,11 @@ RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy do
(id serial not null, created_at timestamptz not null, PRIMARY KEY (id, created_at))
PARTITION BY RANGE (created_at);
- CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.partitioned_test_000000
+ CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_partitioned_test_000000
PARTITION OF #{model.table_name}
FOR VALUES FROM (MINVALUE) TO ('2020-05-01');
- CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.partitioned_test_202006
+ CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_partitioned_test_202006
PARTITION OF #{model.table_name}
FOR VALUES FROM ('2020-06-01') TO ('2020-07-01');
SQL
@@ -166,7 +166,7 @@ RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy do
(id serial not null, created_at timestamptz not null, PRIMARY KEY (id, created_at))
PARTITION BY RANGE (created_at);
- CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.partitioned_test_202006
+ CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_partitioned_test_202006
PARTITION OF #{model.table_name}
FOR VALUES FROM ('2020-06-01') TO ('2020-07-01');
SQL
@@ -181,13 +181,13 @@ RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy do
describe '#extra_partitions' do
let(:model) do
Class.new(ActiveRecord::Base) do
- self.table_name = 'partitioned_test'
+ self.table_name = '_test_partitioned_test'
self.primary_key = :id
end
end
let(:partitioning_key) { :created_at }
- let(:table_name) { :partitioned_test }
+ let(:table_name) { :_test_partitioned_test }
around do |example|
travel_to(Date.parse('2020-08-22')) { example.run }
@@ -200,15 +200,15 @@ RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy do
(id serial not null, created_at timestamptz not null, PRIMARY KEY (id, created_at))
PARTITION BY RANGE (created_at);
- CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.partitioned_test_000000
+ CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_partitioned_test_000000
PARTITION OF #{table_name}
FOR VALUES FROM (MINVALUE) TO ('2020-05-01');
- CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.partitioned_test_202005
+ CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_partitioned_test_202005
PARTITION OF #{table_name}
FOR VALUES FROM ('2020-05-01') TO ('2020-06-01');
- CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.partitioned_test_202006
+ CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_partitioned_test_202006
PARTITION OF #{table_name}
FOR VALUES FROM ('2020-06-01') TO ('2020-07-01')
SQL
@@ -235,7 +235,7 @@ RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy do
it 'prunes the unbounded partition ending 2020-05-01' do
min_value_to_may = Gitlab::Database::Partitioning::TimePartition.new(model.table_name, nil, '2020-05-01',
- partition_name: 'partitioned_test_000000')
+ partition_name: '_test_partitioned_test_000000')
expect(subject).to contain_exactly(min_value_to_may)
end
@@ -246,8 +246,8 @@ RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy do
it 'prunes the unbounded partition and the partition for May-June' do
expect(subject).to contain_exactly(
- Gitlab::Database::Partitioning::TimePartition.new(model.table_name, nil, '2020-05-01', partition_name: 'partitioned_test_000000'),
- Gitlab::Database::Partitioning::TimePartition.new(model.table_name, '2020-05-01', '2020-06-01', partition_name: 'partitioned_test_202005')
+ Gitlab::Database::Partitioning::TimePartition.new(model.table_name, nil, '2020-05-01', partition_name: '_test_partitioned_test_000000'),
+ Gitlab::Database::Partitioning::TimePartition.new(model.table_name, '2020-05-01', '2020-06-01', partition_name: '_test_partitioned_test_202005')
)
end
@@ -256,16 +256,16 @@ RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy do
it 'prunes empty partitions' do
expect(subject).to contain_exactly(
- Gitlab::Database::Partitioning::TimePartition.new(model.table_name, nil, '2020-05-01', partition_name: 'partitioned_test_000000'),
- Gitlab::Database::Partitioning::TimePartition.new(model.table_name, '2020-05-01', '2020-06-01', partition_name: 'partitioned_test_202005')
+ Gitlab::Database::Partitioning::TimePartition.new(model.table_name, nil, '2020-05-01', partition_name: '_test_partitioned_test_000000'),
+ Gitlab::Database::Partitioning::TimePartition.new(model.table_name, '2020-05-01', '2020-06-01', partition_name: '_test_partitioned_test_202005')
)
end
it 'does not prune non-empty partitions' do
- connection.execute("INSERT INTO #{table_name} (created_at) VALUES (('2020-05-15'))") # inserting one record into partitioned_test_202005
+ connection.execute("INSERT INTO #{table_name} (created_at) VALUES (('2020-05-15'))") # inserting one record into _test_partitioned_test_202005
expect(subject).to contain_exactly(
- Gitlab::Database::Partitioning::TimePartition.new(model.table_name, nil, '2020-05-01', partition_name: 'partitioned_test_000000')
+ Gitlab::Database::Partitioning::TimePartition.new(model.table_name, nil, '2020-05-01', partition_name: '_test_partitioned_test_000000')
)
end
end
diff --git a/spec/lib/gitlab/database/partitioning/multi_database_partition_dropper_spec.rb b/spec/lib/gitlab/database/partitioning/multi_database_partition_dropper_spec.rb
deleted file mode 100644
index 56d6ebb7aff..00000000000
--- a/spec/lib/gitlab/database/partitioning/multi_database_partition_dropper_spec.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Database::Partitioning::MultiDatabasePartitionDropper, '#drop_detached_partitions' do
- subject(:drop_detached_partitions) { multi_db_dropper.drop_detached_partitions }
-
- let(:multi_db_dropper) { described_class.new }
-
- let(:connection_wrapper1) { double(scope: scope1) }
- let(:connection_wrapper2) { double(scope: scope2) }
-
- let(:scope1) { double(connection: connection1) }
- let(:scope2) { double(connection: connection2) }
-
- let(:connection1) { double('connection') }
- let(:connection2) { double('connection') }
-
- let(:dropper_class) { Gitlab::Database::Partitioning::DetachedPartitionDropper }
- let(:dropper1) { double('partition dropper') }
- let(:dropper2) { double('partition dropper') }
-
- before do
- allow(multi_db_dropper).to receive(:databases).and_return({ db1: connection_wrapper1, db2: connection_wrapper2 })
- end
-
- it 'drops detached partitions for each database' do
- expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(connection1).and_yield.ordered
- expect(dropper_class).to receive(:new).and_return(dropper1).ordered
- expect(dropper1).to receive(:perform)
-
- expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(connection2).and_yield.ordered
- expect(dropper_class).to receive(:new).and_return(dropper2).ordered
- expect(dropper2).to receive(:perform)
-
- drop_detached_partitions
- end
-end
diff --git a/spec/lib/gitlab/database/partitioning/multi_database_partition_manager_spec.rb b/spec/lib/gitlab/database/partitioning/multi_database_partition_manager_spec.rb
deleted file mode 100644
index 3c94c1bf4ea..00000000000
--- a/spec/lib/gitlab/database/partitioning/multi_database_partition_manager_spec.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Database::Partitioning::MultiDatabasePartitionManager, '#sync_partitions' do
- subject(:sync_partitions) { manager.sync_partitions }
-
- let(:manager) { described_class.new(models) }
- let(:models) { [model1, model2] }
-
- let(:model1) { double('model1', connection: connection1, table_name: 'table1') }
- let(:model2) { double('model2', connection: connection1, table_name: 'table2') }
-
- let(:connection1) { double('connection1') }
- let(:connection2) { double('connection2') }
-
- let(:target_manager_class) { Gitlab::Database::Partitioning::PartitionManager }
- let(:target_manager1) { double('partition manager') }
- let(:target_manager2) { double('partition manager') }
-
- before do
- allow(manager).to receive(:connection_name).and_return('name')
- end
-
- it 'syncs model partitions, setting up the appropriate connection for each', :aggregate_failures do
- expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(model1.connection).and_yield.ordered
- expect(target_manager_class).to receive(:new).with(model1).and_return(target_manager1).ordered
- expect(target_manager1).to receive(:sync_partitions)
-
- expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(model2.connection).and_yield.ordered
- expect(target_manager_class).to receive(:new).with(model2).and_return(target_manager2).ordered
- expect(target_manager2).to receive(:sync_partitions)
-
- sync_partitions
- end
-end
diff --git a/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb b/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb
index 7c4cfcfb3a9..1c6f5c5c694 100644
--- a/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb
+++ b/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb
@@ -195,7 +195,7 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
end
# Postgres 11 does not support foreign keys to partitioned tables
- if Gitlab::Database.main.version.to_f >= 12
+ if ApplicationRecord.database.version.to_f >= 12
context 'when the model is the target of a foreign key' do
before do
connection.execute(<<~SQL)
diff --git a/spec/lib/gitlab/database/partitioning/partition_monitoring_spec.rb b/spec/lib/gitlab/database/partitioning/partition_monitoring_spec.rb
index 7024cbd55ff..006ce8a7f48 100644
--- a/spec/lib/gitlab/database/partitioning/partition_monitoring_spec.rb
+++ b/spec/lib/gitlab/database/partitioning/partition_monitoring_spec.rb
@@ -4,9 +4,8 @@ require 'spec_helper'
RSpec.describe Gitlab::Database::Partitioning::PartitionMonitoring do
describe '#report_metrics' do
- subject { described_class.new(models).report_metrics }
+ subject { described_class.new.report_metrics_for_model(model) }
- let(:models) { [model] }
let(:model) { double(partitioning_strategy: partitioning_strategy, table_name: table) }
let(:partitioning_strategy) { double(missing_partitions: missing_partitions, current_partitions: current_partitions, extra_partitions: extra_partitions) }
let(:table) { "some_table" }
diff --git a/spec/lib/gitlab/database/partitioning/replace_table_spec.rb b/spec/lib/gitlab/database/partitioning/replace_table_spec.rb
index 8e27797208c..fdf514b519f 100644
--- a/spec/lib/gitlab/database/partitioning/replace_table_spec.rb
+++ b/spec/lib/gitlab/database/partitioning/replace_table_spec.rb
@@ -5,7 +5,9 @@ require 'spec_helper'
RSpec.describe Gitlab::Database::Partitioning::ReplaceTable, '#perform' do
include Database::TableSchemaHelpers
- subject(:replace_table) { described_class.new(original_table, replacement_table, archived_table, 'id').perform }
+ subject(:replace_table) do
+ described_class.new(connection, original_table, replacement_table, archived_table, 'id').perform
+ end
let(:original_table) { '_test_original_table' }
let(:replacement_table) { '_test_replacement_table' }
diff --git a/spec/lib/gitlab/database/partitioning_spec.rb b/spec/lib/gitlab/database/partitioning_spec.rb
index 486af9413e8..154cc2b7972 100644
--- a/spec/lib/gitlab/database/partitioning_spec.rb
+++ b/spec/lib/gitlab/database/partitioning_spec.rb
@@ -3,52 +3,175 @@
require 'spec_helper'
RSpec.describe Gitlab::Database::Partitioning do
+ include Database::PartitioningHelpers
+ include Database::TableSchemaHelpers
+
+ let(:connection) { ApplicationRecord.connection }
+
+ around do |example|
+ previously_registered_models = described_class.registered_models.dup
+ described_class.instance_variable_set('@registered_models', Set.new)
+
+ previously_registered_tables = described_class.registered_tables.dup
+ described_class.instance_variable_set('@registered_tables', Set.new)
+
+ example.run
+
+ described_class.instance_variable_set('@registered_models', previously_registered_models)
+ described_class.instance_variable_set('@registered_tables', previously_registered_tables)
+ end
+
+ describe '.register_models' do
+ context 'ensure that the registered models have partitioning strategy' do
+ it 'fails when partitioning_strategy is not specified for the model' do
+ model = Class.new(ApplicationRecord)
+ expect { described_class.register_models([model]) }.to raise_error /should have partitioning strategy defined/
+ end
+ end
+ end
+
+ describe '.sync_partitions_ignore_db_error' do
+ it 'calls sync_partitions' do
+ expect(described_class).to receive(:sync_partitions)
+
+ described_class.sync_partitions_ignore_db_error
+ end
+
+ [ActiveRecord::ActiveRecordError, PG::Error].each do |error|
+ context "when #{error} is raised" do
+ before do
+ expect(described_class).to receive(:sync_partitions)
+ .and_raise(error)
+ end
+
+ it 'ignores it' do
+ described_class.sync_partitions_ignore_db_error
+ end
+ end
+ end
+
+ context 'when DISABLE_POSTGRES_PARTITION_CREATION_ON_STARTUP is set' do
+ before do
+ stub_env('DISABLE_POSTGRES_PARTITION_CREATION_ON_STARTUP', '1')
+ end
+
+ it 'does not call sync_partitions' do
+ expect(described_class).to receive(:sync_partitions).never
+
+ described_class.sync_partitions_ignore_db_error
+ end
+ end
+ end
+
describe '.sync_partitions' do
- let(:partition_manager_class) { described_class::MultiDatabasePartitionManager }
- let(:partition_manager) { double('partition manager') }
+ let(:table_names) { %w[partitioning_test1 partitioning_test2] }
+ let(:models) do
+ table_names.map do |table_name|
+ Class.new(ApplicationRecord) do
+ include PartitionedTable
+
+ self.table_name = table_name
+ partitioned_by :created_at, strategy: :monthly
+ end
+ end
+ end
+
+ before do
+ table_names.each do |table_name|
+ connection.execute(<<~SQL)
+ CREATE TABLE #{table_name} (
+ id serial not null,
+ created_at timestamptz not null,
+ PRIMARY KEY (id, created_at))
+ PARTITION BY RANGE (created_at);
+ SQL
+ end
+ end
+
+ it 'manages partitions for each given model' do
+ expect { described_class.sync_partitions(models)}
+ .to change { find_partitions(table_names.first).size }.from(0)
+ .and change { find_partitions(table_names.last).size }.from(0)
+ end
context 'when no partitioned models are given' do
- it 'calls the partition manager with the registered models' do
- expect(partition_manager_class).to receive(:new)
- .with(described_class.registered_models)
- .and_return(partition_manager)
+ it 'manages partitions for each registered model' do
+ described_class.register_models([models.first])
+ described_class.register_tables([
+ {
+ table_name: table_names.last,
+ partitioned_column: :created_at, strategy: :monthly
+ }
+ ])
- expect(partition_manager).to receive(:sync_partitions)
+ expect { described_class.sync_partitions }
+ .to change { find_partitions(table_names.first).size }.from(0)
+ .and change { find_partitions(table_names.last).size }.from(0)
+ end
+ end
+ end
+
+ describe '.report_metrics' do
+ let(:model1) { double('model') }
+ let(:model2) { double('model') }
+
+ let(:partition_monitoring_class) { described_class::PartitionMonitoring }
+
+ context 'when no partitioned models are given' do
+ it 'reports metrics for each registered model' do
+ expect_next_instance_of(partition_monitoring_class) do |partition_monitor|
+ expect(partition_monitor).to receive(:report_metrics_for_model).with(model1)
+ expect(partition_monitor).to receive(:report_metrics_for_model).with(model2)
+ end
+
+ expect(Gitlab::Database::EachDatabase).to receive(:each_model_connection)
+ .with(described_class.__send__(:registered_models))
+ .and_yield(model1)
+ .and_yield(model2)
- described_class.sync_partitions
+ described_class.report_metrics
end
end
context 'when partitioned models are given' do
- it 'calls the partition manager with the given models' do
- models = ['my special model']
+ it 'reports metrics for each given model' do
+ expect_next_instance_of(partition_monitoring_class) do |partition_monitor|
+ expect(partition_monitor).to receive(:report_metrics_for_model).with(model1)
+ expect(partition_monitor).to receive(:report_metrics_for_model).with(model2)
+ end
- expect(partition_manager_class).to receive(:new)
- .with(models)
- .and_return(partition_manager)
+ expect(Gitlab::Database::EachDatabase).to receive(:each_model_connection)
+ .with([model1, model2])
+ .and_yield(model1)
+ .and_yield(model2)
- expect(partition_manager).to receive(:sync_partitions)
-
- described_class.sync_partitions(models)
+ described_class.report_metrics([model1, model2])
end
end
end
describe '.drop_detached_partitions' do
- let(:partition_dropper_class) { described_class::MultiDatabasePartitionDropper }
+ let(:table_names) { %w[detached_test_partition1 detached_test_partition2] }
+
+ before do
+ table_names.each do |table_name|
+ connection.create_table("#{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.#{table_name}")
- it 'delegates to the partition dropper' do
- expect_next_instance_of(partition_dropper_class) do |partition_dropper|
- expect(partition_dropper).to receive(:drop_detached_partitions)
+ Postgresql::DetachedPartition.create!(table_name: table_name, drop_after: 1.year.ago)
end
+ end
- described_class.drop_detached_partitions
+ it 'drops detached partitions for each database' do
+ expect(Gitlab::Database::EachDatabase).to receive(:each_database_connection).and_yield
+
+ expect { described_class.drop_detached_partitions }
+ .to change { Postgresql::DetachedPartition.count }.from(2).to(0)
+ .and change { table_exists?(table_names.first) }.from(true).to(false)
+ .and change { table_exists?(table_names.last) }.from(true).to(false)
end
- end
- context 'ensure that the registered models have partitioning strategy' do
- it 'fails when partitioning_strategy is not specified for the model' do
- expect(described_class.registered_models).to all(respond_to(:partitioning_strategy))
+ def table_exists?(table_name)
+ table_oid(table_name).present?
end
end
end
diff --git a/spec/lib/gitlab/database/postgres_foreign_key_spec.rb b/spec/lib/gitlab/database/postgres_foreign_key_spec.rb
index ec39e5bfee7..b0e08ca1e67 100644
--- a/spec/lib/gitlab/database/postgres_foreign_key_spec.rb
+++ b/spec/lib/gitlab/database/postgres_foreign_key_spec.rb
@@ -38,4 +38,16 @@ RSpec.describe Gitlab::Database::PostgresForeignKey, type: :model do
expect(described_class.by_referenced_table_identifier('public.referenced_table')).to contain_exactly(expected)
end
end
+
+ describe '#by_constrained_table_identifier' do
+ it 'throws an error when the identifier name is not fully qualified' do
+ expect { described_class.by_constrained_table_identifier('constrained_table') }.to raise_error(ArgumentError, /not fully qualified/)
+ end
+
+ it 'finds the foreign keys for the constrained table' do
+ expected = described_class.where(name: %w[fk_constrained_to_referenced fk_constrained_to_other_referenced]).to_a
+
+ expect(described_class.by_constrained_table_identifier('public.constrained_table')).to match_array(expected)
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/postgres_hll/batch_distinct_counter_spec.rb b/spec/lib/gitlab/database/postgres_hll/batch_distinct_counter_spec.rb
index 2c550f14a08..c9bbc32e059 100644
--- a/spec/lib/gitlab/database/postgres_hll/batch_distinct_counter_spec.rb
+++ b/spec/lib/gitlab/database/postgres_hll/batch_distinct_counter_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe Gitlab::Database::PostgresHll::BatchDistinctCounter do
end
before do
- allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(in_transaction)
+ allow(model.connection).to receive(:transaction_open?).and_return(in_transaction)
end
context 'unit test for different counting parameters' do
diff --git a/spec/lib/gitlab/database/postgres_index_bloat_estimate_spec.rb b/spec/lib/gitlab/database/postgres_index_bloat_estimate_spec.rb
index da4422bd442..13ac9190ab7 100644
--- a/spec/lib/gitlab/database/postgres_index_bloat_estimate_spec.rb
+++ b/spec/lib/gitlab/database/postgres_index_bloat_estimate_spec.rb
@@ -13,6 +13,8 @@ RSpec.describe Gitlab::Database::PostgresIndexBloatEstimate do
let(:identifier) { 'public.schema_migrations_pkey' }
+ it { is_expected.to be_a Gitlab::Database::SharedModel }
+
describe '#bloat_size' do
it 'returns the bloat size in bytes' do
# We cannot reach much more about the bloat size estimate here
diff --git a/spec/lib/gitlab/database/postgres_index_spec.rb b/spec/lib/gitlab/database/postgres_index_spec.rb
index 9088719d5a4..db66736676b 100644
--- a/spec/lib/gitlab/database/postgres_index_spec.rb
+++ b/spec/lib/gitlab/database/postgres_index_spec.rb
@@ -22,6 +22,8 @@ RSpec.describe Gitlab::Database::PostgresIndex do
it_behaves_like 'a postgres model'
+ it { is_expected.to be_a Gitlab::Database::SharedModel }
+
describe '.reindexing_support' do
it 'only non partitioned indexes' do
expect(described_class.reindexing_support).to all(have_attributes(partitioned: false))
diff --git a/spec/lib/gitlab/database/query_analyzer_spec.rb b/spec/lib/gitlab/database/query_analyzer_spec.rb
new file mode 100644
index 00000000000..82a1c7143d5
--- /dev/null
+++ b/spec/lib/gitlab/database/query_analyzer_spec.rb
@@ -0,0 +1,144 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::QueryAnalyzer, query_analyzers: false do
+ let(:analyzer) { double(:query_analyzer) }
+ let(:disabled_analyzer) { double(:disabled_query_analyzer) }
+
+ before do
+ allow(described_class.instance).to receive(:all_analyzers).and_return([analyzer, disabled_analyzer])
+ allow(analyzer).to receive(:enabled?).and_return(true)
+ allow(analyzer).to receive(:suppressed?).and_return(false)
+ allow(analyzer).to receive(:begin!)
+ allow(analyzer).to receive(:end!)
+ allow(disabled_analyzer).to receive(:enabled?).and_return(false)
+ end
+
+ context 'the hook is enabled by default in specs' do
+ it 'does process queries and gets normalized SQL' do
+ expect(analyzer).to receive(:enabled?).and_return(true)
+ expect(analyzer).to receive(:analyze) do |parsed|
+ expect(parsed.sql).to include("SELECT $1 FROM projects")
+ expect(parsed.pg.tables).to eq(%w[projects])
+ end
+
+ described_class.instance.within do
+ Project.connection.execute("SELECT 1 FROM projects")
+ end
+ end
+
+ it 'does prevent recursive execution' do
+ expect(analyzer).to receive(:enabled?).and_return(true)
+ expect(analyzer).to receive(:analyze) do
+ Project.connection.execute("SELECT 1 FROM projects")
+ end
+
+ described_class.instance.within do
+ Project.connection.execute("SELECT 1 FROM projects")
+ end
+ end
+ end
+
+ describe '#within' do
+ context 'when it is already initialized' do
+ around do |example|
+ described_class.instance.within do
+ example.run
+ end
+ end
+
+ it 'does not evaluate enabled? again do yield block' do
+ expect(analyzer).not_to receive(:enabled?)
+
+ expect { |b| described_class.instance.within(&b) }.to yield_control
+ end
+ end
+
+ context 'when initializer is enabled' do
+ before do
+ expect(analyzer).to receive(:enabled?).and_return(true)
+ end
+
+ it 'calls begin! and end!' do
+ expect(analyzer).to receive(:begin!)
+ expect(analyzer).to receive(:end!)
+
+ expect { |b| described_class.instance.within(&b) }.to yield_control
+ end
+
+ it 'when begin! raises the end! is not called' do
+ expect(analyzer).to receive(:begin!).and_raise('exception')
+ expect(analyzer).not_to receive(:end!)
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
+
+ expect { |b| described_class.instance.within(&b) }.to yield_control
+ end
+ end
+ end
+
+ describe '#process_sql' do
+ it 'does not analyze query if not enabled' do
+ expect(analyzer).to receive(:enabled?).and_return(false)
+ expect(analyzer).not_to receive(:analyze)
+
+ process_sql("SELECT 1 FROM projects")
+ end
+
+ it 'does analyze query if enabled' do
+ expect(analyzer).to receive(:enabled?).and_return(true)
+ expect(analyzer).to receive(:analyze) do |parsed|
+ expect(parsed.sql).to eq("SELECT $1 FROM projects")
+ expect(parsed.pg.tables).to eq(%w[projects])
+ end
+
+ process_sql("SELECT 1 FROM projects")
+ end
+
+ it 'does track exception if query cannot be parsed' do
+ expect(analyzer).to receive(:enabled?).and_return(true)
+ expect(analyzer).not_to receive(:analyze)
+ expect(Gitlab::ErrorTracking).to receive(:track_exception)
+
+ expect { process_sql("invalid query") }.not_to raise_error
+ end
+
+ it 'does track exception if analyzer raises exception on enabled?' do
+ expect(analyzer).to receive(:enabled?).and_raise('exception')
+ expect(analyzer).not_to receive(:analyze)
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
+
+ expect { process_sql("SELECT 1 FROM projects") }.not_to raise_error
+ end
+
+ it 'does track exception if analyzer raises exception on analyze' do
+ expect(analyzer).to receive(:enabled?).and_return(true)
+ expect(analyzer).to receive(:analyze).and_raise('exception')
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
+
+ expect { process_sql("SELECT 1 FROM projects") }.not_to raise_error
+ end
+
+ it 'does call analyze only on enabled initializers' do
+ expect(analyzer).to receive(:analyze)
+ expect(disabled_analyzer).not_to receive(:analyze)
+
+ expect { process_sql("SELECT 1 FROM projects") }.not_to raise_error
+ end
+
+ it 'does not call analyze on suppressed analyzers' do
+ expect(analyzer).to receive(:suppressed?).and_return(true)
+ expect(analyzer).not_to receive(:analyze)
+
+ expect { process_sql("SELECT 1 FROM projects") }.not_to raise_error
+ end
+
+ def process_sql(sql)
+ described_class.instance.within do
+ ApplicationRecord.load_balancer.read_write do |connection|
+ described_class.instance.process_sql(sql, connection)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics_spec.rb b/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics_spec.rb
new file mode 100644
index 00000000000..ab5f05e3ec4
--- /dev/null
+++ b/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::QueryAnalyzers::GitlabSchemasMetrics, query_analyzers: false do
+ let(:analyzer) { described_class }
+
+ before do
+ allow(Gitlab::Database::QueryAnalyzer.instance).to receive(:all_analyzers).and_return([analyzer])
+ end
+
+ it 'does not increment metrics if feature flag is disabled' do
+ stub_feature_flags(query_analyzer_gitlab_schema_metrics: false)
+
+ expect(analyzer).not_to receive(:analyze)
+
+ process_sql(ActiveRecord::Base, "SELECT 1 FROM projects")
+ end
+
+ context 'properly observes all queries', :mocked_ci_connection do
+ using RSpec::Parameterized::TableSyntax
+
+ where do
+ {
+ "for simple query observes schema correctly" => {
+ model: ApplicationRecord,
+ sql: "SELECT 1 FROM projects",
+ expectations: {
+ gitlab_schemas: "gitlab_main",
+ db_config_name: "main"
+ }
+ },
+ "for query accessing gitlab_ci and gitlab_main" => {
+ model: ApplicationRecord,
+ sql: "SELECT 1 FROM projects LEFT JOIN ci_builds ON ci_builds.project_id=projects.id",
+ expectations: {
+ gitlab_schemas: "gitlab_ci,gitlab_main",
+ db_config_name: "main"
+ }
+ },
+ "for query accessing gitlab_ci and gitlab_main the gitlab_schemas is always ordered" => {
+ model: ApplicationRecord,
+ sql: "SELECT 1 FROM ci_builds LEFT JOIN projects ON ci_builds.project_id=projects.id",
+ expectations: {
+ gitlab_schemas: "gitlab_ci,gitlab_main",
+ db_config_name: "main"
+ }
+ },
+ "for query accessing CI database" => {
+ model: Ci::ApplicationRecord,
+ sql: "SELECT 1 FROM ci_builds",
+ expectations: {
+ gitlab_schemas: "gitlab_ci",
+ db_config_name: "ci"
+ }
+ }
+ }
+ end
+
+ with_them do
+ around do |example|
+ Gitlab::Database::QueryAnalyzer.instance.within { example.run }
+ end
+
+ it do
+ expect(described_class.schemas_metrics).to receive(:increment)
+ .with(expectations).and_call_original
+
+ process_sql(model, sql)
+ end
+ end
+ end
+
+ def process_sql(model, sql)
+ Gitlab::Database::QueryAnalyzer.instance.within do
+ # Skip load balancer and retrieve connection assigned to model
+ Gitlab::Database::QueryAnalyzer.instance.process_sql(sql, model.retrieve_connection)
+ end
+ end
+end
diff --git a/spec/support_specs/database/prevent_cross_database_modification_spec.rb b/spec/lib/gitlab/database/query_analyzers/prevent_cross_database_modification_spec.rb
index e86559bb14a..eb8ccb0bd89 100644
--- a/spec/support_specs/database/prevent_cross_database_modification_spec.rb
+++ b/spec/lib/gitlab/database/query_analyzers/prevent_cross_database_modification_spec.rb
@@ -2,11 +2,19 @@
require 'spec_helper'
-RSpec.describe 'Database::PreventCrossDatabaseModification' do
+RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification, query_analyzers: false do
let_it_be(:pipeline, refind: true) { create(:ci_pipeline) }
let_it_be(:project, refind: true) { create(:project) }
- shared_examples 'succeessful examples' do
+ before do
+ allow(Gitlab::Database::QueryAnalyzer.instance).to receive(:all_analyzers).and_return([described_class])
+ end
+
+ around do |example|
+ Gitlab::Database::QueryAnalyzer.instance.within { example.run }
+ end
+
+ shared_examples 'successful examples' do
context 'outside transaction' do
it { expect { run_queries }.not_to raise_error }
end
@@ -36,7 +44,7 @@ RSpec.describe 'Database::PreventCrossDatabaseModification' do
project.reload
end
- include_examples 'succeessful examples'
+ include_examples 'successful examples'
end
context 'when only CI data is modified' do
@@ -45,7 +53,7 @@ RSpec.describe 'Database::PreventCrossDatabaseModification' do
project.reload
end
- include_examples 'succeessful examples'
+ include_examples 'successful examples'
end
context 'when other data is modified' do
@@ -54,27 +62,42 @@ RSpec.describe 'Database::PreventCrossDatabaseModification' do
project.touch
end
- include_examples 'succeessful examples'
+ include_examples 'successful examples'
end
- describe 'with_cross_database_modification_prevented block' do
- it 'raises error when CI and other data is modified' do
- expect do
- with_cross_database_modification_prevented do
- Project.transaction do
+ context 'when both CI and other data is modified' do
+ def run_queries
+ project.touch
+ pipeline.touch
+ end
+
+ context 'outside transaction' do
+ it { expect { run_queries }.not_to raise_error }
+ end
+
+ context 'when data modification happens in a transaction' do
+ it 'raises error' do
+ Project.transaction do
+ expect { run_queries }.to raise_error /Cross-database data modification/
+ end
+ end
+
+ context 'when data modification happens in nested transactions' do
+ it 'raises error' do
+ Project.transaction(requires_new: true) do
project.touch
- pipeline.touch
+ Project.transaction(requires_new: true) do
+ expect { pipeline.touch }.to raise_error /Cross-database data modification/
+ end
end
end
- end.to raise_error /Cross-database data modification/
+ end
end
- end
- context 'when running tests with prevent_cross_database_modification', :prevent_cross_database_modification do
- context 'when both CI and other data is modified' do
+ context 'when executing a SELECT FOR UPDATE query' do
def run_queries
project.touch
- pipeline.touch
+ pipeline.lock!
end
context 'outside transaction' do
@@ -88,33 +111,11 @@ RSpec.describe 'Database::PreventCrossDatabaseModification' do
end
end
- context 'when data modification happens in nested transactions' do
- it 'raises error' do
- Project.transaction(requires_new: true) do
- project.touch
- Project.transaction(requires_new: true) do
- expect { pipeline.touch }.to raise_error /Cross-database data modification/
- end
- end
- end
- end
- end
+ context 'when the modification is inside a factory save! call' do
+ let(:runner) { create(:ci_runner, :project, projects: [build(:project)]) }
- context 'when executing a SELECT FOR UPDATE query' do
- def run_queries
- project.touch
- pipeline.lock!
- end
-
- context 'outside transaction' do
- it { expect { run_queries }.not_to raise_error }
- end
-
- context 'when data modification happens in a transaction' do
- it 'raises error' do
- Project.transaction do
- expect { run_queries }.to raise_error /Cross-database data modification/
- end
+ it 'does not raise an error' do
+ runner
end
end
end
@@ -126,13 +127,13 @@ RSpec.describe 'Database::PreventCrossDatabaseModification' do
project.save!
end
- include_examples 'succeessful examples'
+ include_examples 'successful examples'
end
- describe '#allow_cross_database_modification_within_transaction' do
+ describe '.allow_cross_database_modification_within_transaction' do
it 'skips raising error' do
expect do
- Gitlab::Database.allow_cross_database_modification_within_transaction(url: 'gitlab-issue') do
+ described_class.allow_cross_database_modification_within_transaction(url: 'gitlab-issue') do
Project.transaction do
pipeline.touch
project.touch
@@ -141,17 +142,9 @@ RSpec.describe 'Database::PreventCrossDatabaseModification' do
end.not_to raise_error
end
- it 'raises error when complex factories are built referencing both databases' do
- expect do
- ApplicationRecord.transaction do
- create(:ci_pipeline)
- end
- end.to raise_error /Cross-database data modification/
- end
-
it 'skips raising error on factory creation' do
expect do
- Gitlab::Database.allow_cross_database_modification_within_transaction(url: 'gitlab-issue') do
+ described_class.allow_cross_database_modification_within_transaction(url: 'gitlab-issue') do
ApplicationRecord.transaction do
create(:ci_pipeline)
end
@@ -160,4 +153,15 @@ RSpec.describe 'Database::PreventCrossDatabaseModification' do
end
end
end
+
+ context 'when some table with a defined schema and another table with undefined gitlab_schema is modified' do
+ it 'raises an error including including message about undefined schema' do
+ expect do
+ Project.transaction do
+ project.touch
+ project.connection.execute('UPDATE foo_bars_undefined_table SET a=1 WHERE id = -1')
+ end
+ end.to raise_error /Cross-database data modification.*The gitlab_schema was undefined/
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/reflection_spec.rb b/spec/lib/gitlab/database/reflection_spec.rb
new file mode 100644
index 00000000000..7c3d797817d
--- /dev/null
+++ b/spec/lib/gitlab/database/reflection_spec.rb
@@ -0,0 +1,280 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::Reflection do
+ let(:database) { described_class.new(ApplicationRecord) }
+
+ describe '#username' do
+ context 'when a username is set' do
+ it 'returns the username' do
+ allow(database).to receive(:config).and_return(username: 'bob')
+
+ expect(database.username).to eq('bob')
+ end
+ end
+
+ context 'when a username is not set' do
+ it 'returns the value of the USER environment variable' do
+ allow(database).to receive(:config).and_return(username: nil)
+ allow(ENV).to receive(:[]).with('USER').and_return('bob')
+
+ expect(database.username).to eq('bob')
+ end
+ end
+ end
+
+ describe '#database_name' do
+ it 'returns the name of the database' do
+ allow(database).to receive(:config).and_return(database: 'test')
+
+ expect(database.database_name).to eq('test')
+ end
+ end
+
+ describe '#adapter_name' do
+ it 'returns the database adapter name' do
+ allow(database).to receive(:config).and_return(adapter: 'test')
+
+ expect(database.adapter_name).to eq('test')
+ end
+ end
+
+ describe '#human_adapter_name' do
+ context 'when the adapter is PostgreSQL' do
+ it 'returns PostgreSQL' do
+ allow(database).to receive(:config).and_return(adapter: 'postgresql')
+
+ expect(database.human_adapter_name).to eq('PostgreSQL')
+ end
+ end
+
+ context 'when the adapter is not PostgreSQL' do
+ it 'returns Unknown' do
+ allow(database).to receive(:config).and_return(adapter: 'kittens')
+
+ expect(database.human_adapter_name).to eq('Unknown')
+ end
+ end
+ end
+
+ describe '#postgresql?' do
+ context 'when using PostgreSQL' do
+ it 'returns true' do
+ allow(database).to receive(:adapter_name).and_return('PostgreSQL')
+
+ expect(database.postgresql?).to eq(true)
+ end
+ end
+
+ context 'when not using PostgreSQL' do
+ it 'returns false' do
+ allow(database).to receive(:adapter_name).and_return('MySQL')
+
+ expect(database.postgresql?).to eq(false)
+ end
+ end
+ end
+
+ describe '#db_read_only?' do
+ it 'detects a read-only database' do
+ allow(database.model.connection)
+ .to receive(:execute)
+ .with('SELECT pg_is_in_recovery()')
+ .and_return([{ "pg_is_in_recovery" => "t" }])
+
+ expect(database.db_read_only?).to be_truthy
+ end
+
+ it 'detects a read-only database' do
+ allow(database.model.connection)
+ .to receive(:execute)
+ .with('SELECT pg_is_in_recovery()')
+ .and_return([{ "pg_is_in_recovery" => true }])
+
+ expect(database.db_read_only?).to be_truthy
+ end
+
+ it 'detects a read-write database' do
+ allow(database.model.connection)
+ .to receive(:execute)
+ .with('SELECT pg_is_in_recovery()')
+ .and_return([{ "pg_is_in_recovery" => "f" }])
+
+ expect(database.db_read_only?).to be_falsey
+ end
+
+ it 'detects a read-write database' do
+ allow(database.model.connection)
+ .to receive(:execute)
+ .with('SELECT pg_is_in_recovery()')
+ .and_return([{ "pg_is_in_recovery" => false }])
+
+ expect(database.db_read_only?).to be_falsey
+ end
+ end
+
+ describe '#db_read_write?' do
+ it 'detects a read-only database' do
+ allow(database.model.connection)
+ .to receive(:execute)
+ .with('SELECT pg_is_in_recovery()')
+ .and_return([{ "pg_is_in_recovery" => "t" }])
+
+ expect(database.db_read_write?).to eq(false)
+ end
+
+ it 'detects a read-only database' do
+ allow(database.model.connection)
+ .to receive(:execute)
+ .with('SELECT pg_is_in_recovery()')
+ .and_return([{ "pg_is_in_recovery" => true }])
+
+ expect(database.db_read_write?).to eq(false)
+ end
+
+ it 'detects a read-write database' do
+ allow(database.model.connection)
+ .to receive(:execute)
+ .with('SELECT pg_is_in_recovery()')
+ .and_return([{ "pg_is_in_recovery" => "f" }])
+
+ expect(database.db_read_write?).to eq(true)
+ end
+
+ it 'detects a read-write database' do
+ allow(database.model.connection)
+ .to receive(:execute)
+ .with('SELECT pg_is_in_recovery()')
+ .and_return([{ "pg_is_in_recovery" => false }])
+
+ expect(database.db_read_write?).to eq(true)
+ end
+ end
+
+ describe '#version' do
+ around do |example|
+ database.instance_variable_set(:@version, nil)
+ example.run
+ database.instance_variable_set(:@version, nil)
+ end
+
+ context "on postgresql" do
+ it "extracts the version number" do
+ allow(database)
+ .to receive(:database_version)
+ .and_return("PostgreSQL 9.4.4 on x86_64-apple-darwin14.3.0")
+
+ expect(database.version).to eq '9.4.4'
+ end
+ end
+
+ it 'memoizes the result' do
+ count = ActiveRecord::QueryRecorder
+ .new { 2.times { database.version } }
+ .count
+
+ expect(count).to eq(1)
+ end
+ end
+
+ describe '#postgresql_minimum_supported_version?' do
+ it 'returns false when using PostgreSQL 10' do
+ allow(database).to receive(:version).and_return('10')
+
+ expect(database.postgresql_minimum_supported_version?).to eq(false)
+ end
+
+ it 'returns false when using PostgreSQL 11' do
+ allow(database).to receive(:version).and_return('11')
+
+ expect(database.postgresql_minimum_supported_version?).to eq(false)
+ end
+
+ it 'returns true when using PostgreSQL 12' do
+ allow(database).to receive(:version).and_return('12')
+
+ expect(database.postgresql_minimum_supported_version?).to eq(true)
+ end
+ end
+
+ describe '#cached_column_exists?' do
+ it 'only retrieves the data from the schema cache' do
+ database = described_class.new(Project)
+ queries = ActiveRecord::QueryRecorder.new do
+ 2.times do
+ expect(database.cached_column_exists?(:id)).to be_truthy
+ expect(database.cached_column_exists?(:bogus_column)).to be_falsey
+ end
+ end
+
+ expect(queries.count).to eq(0)
+ end
+ end
+
+ describe '#cached_table_exists?' do
+ it 'only retrieves the data from the schema cache' do
+ dummy = Class.new(ActiveRecord::Base) do
+ self.table_name = 'bogus_table_name'
+ end
+
+ queries = ActiveRecord::QueryRecorder.new do
+ 2.times do
+ expect(described_class.new(Project).cached_table_exists?).to be_truthy
+ expect(described_class.new(dummy).cached_table_exists?).to be_falsey
+ end
+ end
+
+ expect(queries.count).to eq(0)
+ end
+
+ it 'returns false when database does not exist' do
+ database = described_class.new(Project)
+
+ expect(database.model).to receive(:connection) do
+ raise ActiveRecord::NoDatabaseError, 'broken'
+ end
+
+ expect(database.cached_table_exists?).to be(false)
+ end
+ end
+
+ describe '#exists?' do
+ it 'returns true if the database exists' do
+ expect(database.exists?).to be(true)
+ end
+
+ it "returns false if the database doesn't exist" do
+ expect(database.model.connection.schema_cache)
+ .to receive(:database_version)
+ .and_raise(ActiveRecord::NoDatabaseError)
+
+ expect(database.exists?).to be(false)
+ end
+ end
+
+ describe '#system_id' do
+ it 'returns the PostgreSQL system identifier' do
+ expect(database.system_id).to be_an_instance_of(Integer)
+ end
+ end
+
+ describe '#config' do
+ it 'returns a HashWithIndifferentAccess' do
+ expect(database.config)
+ .to be_an_instance_of(HashWithIndifferentAccess)
+ end
+
+ it 'returns a default pool size' do
+ expect(database.config)
+ .to include(pool: Gitlab::Database.default_pool_size)
+ end
+
+ it 'does not cache its results' do
+ a = database.config
+ b = database.config
+
+ expect(a).not_to equal(b)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/reindexing/index_selection_spec.rb b/spec/lib/gitlab/database/reindexing/index_selection_spec.rb
index ee3f2b1b415..2ae9037959d 100644
--- a/spec/lib/gitlab/database/reindexing/index_selection_spec.rb
+++ b/spec/lib/gitlab/database/reindexing/index_selection_spec.rb
@@ -46,14 +46,14 @@ RSpec.describe Gitlab::Database::Reindexing::IndexSelection do
expect(subject).not_to include(excluded.index)
end
- it 'excludes indexes larger than 100 GB ondisk size' do
- excluded = create(
+ it 'includes indexes larger than 100 GB ondisk size' do
+ included = create(
:postgres_index_bloat_estimate,
index: create(:postgres_index, ondisk_size_bytes: 101.gigabytes),
bloat_size_bytes: 25.gigabyte
)
- expect(subject).not_to include(excluded.index)
+ expect(subject).to include(included.index)
end
context 'with time frozen' do
diff --git a/spec/lib/gitlab/database/reindexing/reindex_action_spec.rb b/spec/lib/gitlab/database/reindexing/reindex_action_spec.rb
index a8f196d8f0e..1b409924acc 100644
--- a/spec/lib/gitlab/database/reindexing/reindex_action_spec.rb
+++ b/spec/lib/gitlab/database/reindexing/reindex_action_spec.rb
@@ -11,6 +11,8 @@ RSpec.describe Gitlab::Database::Reindexing::ReindexAction do
swapout_view_for_table(:postgres_indexes)
end
+ it { is_expected.to be_a Gitlab::Database::SharedModel }
+
describe '.create_for' do
subject { described_class.create_for(index) }
diff --git a/spec/lib/gitlab/database/reindexing/reindex_concurrently_spec.rb b/spec/lib/gitlab/database/reindexing/reindex_concurrently_spec.rb
index 6f87475fc94..db267ff4f14 100644
--- a/spec/lib/gitlab/database/reindexing/reindex_concurrently_spec.rb
+++ b/spec/lib/gitlab/database/reindexing/reindex_concurrently_spec.rb
@@ -62,7 +62,7 @@ RSpec.describe Gitlab::Database::Reindexing::ReindexConcurrently, '#perform' do
it 'recreates the index using REINDEX with a long statement timeout' do
expect_to_execute_in_order(
- "SET statement_timeout TO '32400s'",
+ "SET statement_timeout TO '86400s'",
"REINDEX INDEX CONCURRENTLY \"public\".\"#{index.name}\"",
"RESET statement_timeout"
)
@@ -84,7 +84,7 @@ RSpec.describe Gitlab::Database::Reindexing::ReindexConcurrently, '#perform' do
it 'drops the dangling indexes while controlling lock_timeout' do
expect_to_execute_in_order(
# Regular index rebuild
- "SET statement_timeout TO '32400s'",
+ "SET statement_timeout TO '86400s'",
"REINDEX INDEX CONCURRENTLY \"public\".\"#{index_name}\"",
"RESET statement_timeout",
# Drop _ccnew index
diff --git a/spec/lib/gitlab/database/reindexing_spec.rb b/spec/lib/gitlab/database/reindexing_spec.rb
index 550f9db2b5b..13aff343432 100644
--- a/spec/lib/gitlab/database/reindexing_spec.rb
+++ b/spec/lib/gitlab/database/reindexing_spec.rb
@@ -4,10 +4,63 @@ require 'spec_helper'
RSpec.describe Gitlab::Database::Reindexing do
include ExclusiveLeaseHelpers
+ include Database::DatabaseHelpers
- describe '.perform' do
- subject { described_class.perform(candidate_indexes) }
+ describe '.automatic_reindexing' do
+ subject { described_class.automatic_reindexing(maximum_records: limit) }
+ let(:limit) { 5 }
+
+ before_all do
+ swapout_view_for_table(:postgres_indexes)
+ end
+
+ before do
+ allow(Gitlab::Database::Reindexing).to receive(:cleanup_leftovers!)
+ allow(Gitlab::Database::Reindexing).to receive(:perform_from_queue).and_return(0)
+ allow(Gitlab::Database::Reindexing).to receive(:perform_with_heuristic).and_return(0)
+ end
+
+ it 'cleans up leftovers, before consuming the queue' do
+ expect(Gitlab::Database::Reindexing).to receive(:cleanup_leftovers!).ordered
+ expect(Gitlab::Database::Reindexing).to receive(:perform_from_queue).ordered
+
+ subject
+ end
+
+ context 'with records in the queue' do
+ before do
+ create(:reindexing_queued_action)
+ end
+
+ context 'with enough records in the queue to reach limit' do
+ let(:limit) { 1 }
+
+ it 'does not perform reindexing with heuristic' do
+ expect(Gitlab::Database::Reindexing).to receive(:perform_from_queue).and_return(limit)
+ expect(Gitlab::Database::Reindexing).not_to receive(:perform_with_heuristic)
+
+ subject
+ end
+ end
+
+ context 'without enough records in the queue to reach limit' do
+ let(:limit) { 2 }
+
+ it 'continues if the queue did not have enough records' do
+ expect(Gitlab::Database::Reindexing).to receive(:perform_from_queue).ordered.and_return(1)
+ expect(Gitlab::Database::Reindexing).to receive(:perform_with_heuristic).with(maximum_records: 1).ordered
+
+ subject
+ end
+ end
+ end
+ end
+
+ describe '.perform_with_heuristic' do
+ subject { described_class.perform_with_heuristic(candidate_indexes, maximum_records: limit) }
+
+ let(:limit) { 2 }
let(:coordinator) { instance_double(Gitlab::Database::Reindexing::Coordinator) }
let(:index_selection) { instance_double(Gitlab::Database::Reindexing::IndexSelection) }
let(:candidate_indexes) { double }
@@ -15,7 +68,7 @@ RSpec.describe Gitlab::Database::Reindexing do
it 'delegates to Coordinator' do
expect(Gitlab::Database::Reindexing::IndexSelection).to receive(:new).with(candidate_indexes).and_return(index_selection)
- expect(index_selection).to receive(:take).with(2).and_return(indexes)
+ expect(index_selection).to receive(:take).with(limit).and_return(indexes)
indexes.each do |index|
expect(Gitlab::Database::Reindexing::Coordinator).to receive(:new).with(index).and_return(coordinator)
@@ -26,6 +79,59 @@ RSpec.describe Gitlab::Database::Reindexing do
end
end
+ describe '.perform_from_queue' do
+ subject { described_class.perform_from_queue(maximum_records: limit) }
+
+ before_all do
+ swapout_view_for_table(:postgres_indexes)
+ end
+
+ let(:limit) { 2 }
+ let(:queued_actions) { create_list(:reindexing_queued_action, 3) }
+ let(:coordinator) { instance_double(Gitlab::Database::Reindexing::Coordinator) }
+
+ before do
+ queued_actions.take(limit).each do |action|
+ allow(Gitlab::Database::Reindexing::Coordinator).to receive(:new).with(action.index).and_return(coordinator)
+ allow(coordinator).to receive(:perform)
+ end
+ end
+
+ it 'consumes the queue in order of created_at and applies the limit' do
+ queued_actions.take(limit).each do |action|
+ expect(Gitlab::Database::Reindexing::Coordinator).to receive(:new).ordered.with(action.index).and_return(coordinator)
+ expect(coordinator).to receive(:perform)
+ end
+
+ subject
+ end
+
+ it 'updates queued action and sets state to done' do
+ subject
+
+ queue = queued_actions
+
+ queue.shift(limit).each do |action|
+ expect(action.reload.state).to eq('done')
+ end
+
+ queue.each do |action|
+ expect(action.reload.state).to eq('queued')
+ end
+ end
+
+ it 'updates queued action upon error and sets state to failed' do
+ expect(Gitlab::Database::Reindexing::Coordinator).to receive(:new).ordered.with(queued_actions.first.index).and_return(coordinator)
+ expect(coordinator).to receive(:perform).and_raise('something went wrong')
+
+ subject
+
+ states = queued_actions.map(&:reload).map(&:state)
+
+ expect(states).to eq(%w(failed done queued))
+ end
+ end
+
describe '.cleanup_leftovers!' do
subject { described_class.cleanup_leftovers! }
diff --git a/spec/lib/gitlab/database/schema_cache_with_renamed_table_spec.rb b/spec/lib/gitlab/database/schema_cache_with_renamed_table_spec.rb
index 8c0c4155ccc..7caee414719 100644
--- a/spec/lib/gitlab/database/schema_cache_with_renamed_table_spec.rb
+++ b/spec/lib/gitlab/database/schema_cache_with_renamed_table_spec.rb
@@ -11,12 +11,12 @@ RSpec.describe Gitlab::Database::SchemaCacheWithRenamedTable do
let(:new_model) do
Class.new(ActiveRecord::Base) do
- self.table_name = 'projects_new'
+ self.table_name = '_test_projects_new'
end
end
before do
- stub_const('Gitlab::Database::TABLES_TO_BE_RENAMED', { 'projects' => 'projects_new' })
+ stub_const('Gitlab::Database::TABLES_TO_BE_RENAMED', { 'projects' => '_test_projects_new' })
end
context 'when table is not renamed yet' do
@@ -32,8 +32,8 @@ RSpec.describe Gitlab::Database::SchemaCacheWithRenamedTable do
context 'when table is renamed' do
before do
- ActiveRecord::Base.connection.execute("ALTER TABLE projects RENAME TO projects_new")
- ActiveRecord::Base.connection.execute("CREATE VIEW projects AS SELECT * FROM projects_new")
+ ActiveRecord::Base.connection.execute("ALTER TABLE projects RENAME TO _test_projects_new")
+ ActiveRecord::Base.connection.execute("CREATE VIEW projects AS SELECT * FROM _test_projects_new")
old_model.reset_column_information
ActiveRecord::Base.connection.schema_cache.clear!
@@ -54,14 +54,14 @@ RSpec.describe Gitlab::Database::SchemaCacheWithRenamedTable do
it 'has the same indexes' do
indexes_for_old_table = ActiveRecord::Base.connection.schema_cache.indexes('projects')
- indexes_for_new_table = ActiveRecord::Base.connection.schema_cache.indexes('projects_new')
+ indexes_for_new_table = ActiveRecord::Base.connection.schema_cache.indexes('_test_projects_new')
expect(indexes_for_old_table).to eq(indexes_for_new_table)
end
it 'has the same column_hash' do
columns_hash_for_old_table = ActiveRecord::Base.connection.schema_cache.columns_hash('projects')
- columns_hash_for_new_table = ActiveRecord::Base.connection.schema_cache.columns_hash('projects_new')
+ columns_hash_for_new_table = ActiveRecord::Base.connection.schema_cache.columns_hash('_test_projects_new')
expect(columns_hash_for_old_table).to eq(columns_hash_for_new_table)
end
diff --git a/spec/lib/gitlab/database/schema_migrations/context_spec.rb b/spec/lib/gitlab/database/schema_migrations/context_spec.rb
index 0323fa22b78..07c97ea0ec3 100644
--- a/spec/lib/gitlab/database/schema_migrations/context_spec.rb
+++ b/spec/lib/gitlab/database/schema_migrations/context_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe Gitlab::Database::SchemaMigrations::Context do
end
context 'CI database' do
- let(:connection_class) { Ci::CiDatabaseRecord }
+ let(:connection_class) { Ci::ApplicationRecord }
it 'returns a directory path that is database specific' do
skip_if_multiple_databases_not_setup
diff --git a/spec/lib/gitlab/database/shared_model_spec.rb b/spec/lib/gitlab/database/shared_model_spec.rb
index 5d616aeb05f..94f2b5a3434 100644
--- a/spec/lib/gitlab/database/shared_model_spec.rb
+++ b/spec/lib/gitlab/database/shared_model_spec.rb
@@ -27,6 +27,38 @@ RSpec.describe Gitlab::Database::SharedModel do
end
end
+ context 'when multiple connection overrides are nested', :aggregate_failures do
+ let(:second_connection) { double('connection') }
+
+ it 'allows the nesting with the same connection object' do
+ expect_original_connection_around do
+ described_class.using_connection(new_connection) do
+ expect(described_class.connection).to be(new_connection)
+
+ described_class.using_connection(new_connection) do
+ expect(described_class.connection).to be(new_connection)
+ end
+
+ expect(described_class.connection).to be(new_connection)
+ end
+ end
+ end
+
+ it 'raises an error if the connection is changed' do
+ expect_original_connection_around do
+ described_class.using_connection(new_connection) do
+ expect(described_class.connection).to be(new_connection)
+
+ expect do
+ described_class.using_connection(second_connection) {}
+ end.to raise_error(/cannot nest connection overrides/)
+
+ expect(described_class.connection).to be(new_connection)
+ end
+ end
+ end
+ end
+
context 'when the block raises an error', :aggregate_failures do
it 're-raises the error, removing the overridden connection' do
expect_original_connection_around do
diff --git a/spec/lib/gitlab/database/unidirectional_copy_trigger_spec.rb b/spec/lib/gitlab/database/unidirectional_copy_trigger_spec.rb
index 2955c208f16..bbddb5f1af5 100644
--- a/spec/lib/gitlab/database/unidirectional_copy_trigger_spec.rb
+++ b/spec/lib/gitlab/database/unidirectional_copy_trigger_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Gitlab::Database::UnidirectionalCopyTrigger do
let(:table_name) { '_test_table' }
let(:connection) { ActiveRecord::Base.connection }
- let(:copy_trigger) { described_class.on_table(table_name) }
+ let(:copy_trigger) { described_class.on_table(table_name, connection: connection) }
describe '#name' do
context 'when a single column name is given' do
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index a2e7b6d27b9..5ec7c338a2a 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -15,13 +15,6 @@ RSpec.describe Gitlab::Database do
end
end
- describe '.databases' do
- it 'stores connections as a HashWithIndifferentAccess' do
- expect(described_class.databases.has_key?('main')).to be true
- expect(described_class.databases.has_key?(:main)).to be true
- end
- end
-
describe '.default_pool_size' do
before do
allow(Gitlab::Runtime).to receive(:max_threads).and_return(7)
@@ -112,18 +105,30 @@ RSpec.describe Gitlab::Database do
end
describe '.check_postgres_version_and_print_warning' do
+ let(:reflect) { instance_spy(Gitlab::Database::Reflection) }
+
subject { described_class.check_postgres_version_and_print_warning }
+ before do
+ allow(Gitlab::Database::Reflection)
+ .to receive(:new)
+ .and_return(reflect)
+ end
+
it 'prints a warning if not compliant with minimum postgres version' do
- allow(described_class.main).to receive(:postgresql_minimum_supported_version?).and_return(false)
+ allow(reflect).to receive(:postgresql_minimum_supported_version?).and_return(false)
- expect(Kernel).to receive(:warn).with(/You are using PostgreSQL/)
+ expect(Kernel)
+ .to receive(:warn)
+ .with(/You are using PostgreSQL/)
+ .exactly(Gitlab::Database.database_base_models.length)
+ .times
subject
end
it 'doesnt print a warning if compliant with minimum postgres version' do
- allow(described_class.main).to receive(:postgresql_minimum_supported_version?).and_return(true)
+ allow(reflect).to receive(:postgresql_minimum_supported_version?).and_return(true)
expect(Kernel).not_to receive(:warn).with(/You are using PostgreSQL/)
@@ -131,7 +136,7 @@ RSpec.describe Gitlab::Database do
end
it 'doesnt print a warning in Rails runner environment' do
- allow(described_class.main).to receive(:postgresql_minimum_supported_version?).and_return(false)
+ allow(reflect).to receive(:postgresql_minimum_supported_version?).and_return(false)
allow(Gitlab::Runtime).to receive(:rails_runner?).and_return(true)
expect(Kernel).not_to receive(:warn).with(/You are using PostgreSQL/)
@@ -140,13 +145,13 @@ RSpec.describe Gitlab::Database do
end
it 'ignores ActiveRecord errors' do
- allow(described_class.main).to receive(:postgresql_minimum_supported_version?).and_raise(ActiveRecord::ActiveRecordError)
+ allow(reflect).to receive(:postgresql_minimum_supported_version?).and_raise(ActiveRecord::ActiveRecordError)
expect { subject }.not_to raise_error
end
it 'ignores Postgres errors' do
- allow(described_class.main).to receive(:postgresql_minimum_supported_version?).and_raise(PG::Error)
+ allow(reflect).to receive(:postgresql_minimum_supported_version?).and_raise(PG::Error)
expect { subject }.not_to raise_error
end
@@ -205,7 +210,7 @@ RSpec.describe Gitlab::Database do
context 'when replicas are configured', :database_replica do
it 'returns the name for a replica' do
- replica = ActiveRecord::Base.connection.load_balancer.host
+ replica = ActiveRecord::Base.load_balancer.host
expect(described_class.db_config_name(replica)).to eq('main_replica')
end
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index 1800d2d6b60..4b437397688 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -51,6 +51,48 @@ RSpec.describe Gitlab::Diff::File do
project.commit(branch_name).diffs.diff_files.first
end
+ describe 'initialize' do
+ context 'when file is ipynb with a change after transformation' do
+ let(:commit) { project.commit("f6b7a707") }
+ let(:diff) { commit.raw_diffs.first }
+ let(:diff_file) { described_class.new(diff, diff_refs: commit.diff_refs, repository: project.repository) }
+
+ context 'and :jupyter_clean_diffs is enabled' do
+ before do
+ stub_feature_flags(jupyter_clean_diffs: true)
+ end
+
+ it 'recreates the diff by transforming the files' do
+ expect(diff_file.diff.diff).not_to include('"| Fake')
+ end
+ end
+
+ context 'but :jupyter_clean_diffs is disabled' do
+ before do
+ stub_feature_flags(jupyter_clean_diffs: false)
+ end
+
+ it 'does not recreate the diff' do
+ expect(diff_file.diff.diff).to include('"| Fake')
+ end
+ end
+ end
+
+ context 'when file is ipynb, but there only changes that are removed' do
+ let(:commit) { project.commit("2b5ef814") }
+ let(:diff) { commit.raw_diffs.first }
+ let(:diff_file) { described_class.new(diff, diff_refs: commit.diff_refs, repository: project.repository) }
+
+ before do
+ stub_feature_flags(jupyter_clean_diffs: true)
+ end
+
+ it 'does not recreate the diff' do
+ expect(diff_file.diff.diff).to include('execution_count')
+ end
+ end
+ end
+
describe '#diff_lines' do
let(:diff_lines) { diff_file.diff_lines }
diff --git a/spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb b/spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb
index bdeaabec1f1..b646cf38178 100644
--- a/spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb
+++ b/spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb
@@ -581,13 +581,16 @@ RSpec.describe Gitlab::Diff::PositionTracer::LineStrategy, :clean_gitlab_redis_c
)
end
- it "returns the new position but drops line_range information" do
+ it "returns the new position" do
expect_change_position(
old_path: file_name,
new_path: file_name,
old_line: nil,
new_line: 2,
- line_range: nil
+ line_range: {
+ "start_line_code" => 1,
+ "end_line_code" => 2
+ }
)
end
end
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 8cb1ccc065b..c579027788d 100644
--- a/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb
@@ -11,6 +11,7 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do
end
let(:email_raw) { email_fixture('emails/service_desk.eml') }
+ let(:author_email) { 'jake@adventuretime.ooo' }
let_it_be(:group) { create(:group, :private, name: "email") }
let(:expected_description) do
@@ -45,7 +46,7 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do
receiver.execute
new_issue = Issue.last
- expect(new_issue.issue_email_participants.first.email).to eq("jake@adventuretime.ooo")
+ expect(new_issue.issue_email_participants.first.email).to eq(author_email)
end
it 'sends thank you email' do
@@ -196,60 +197,123 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do
end
end
- context 'when using service desk key' do
- let_it_be(:service_desk_key) { 'mykey' }
+ context 'when all lines of email are quoted' do
+ let(:email_raw) { email_fixture('emails/service_desk_all_quoted.eml') }
- let(:email_raw) { service_desk_fixture('emails/service_desk_custom_address.eml') }
+ it 'creates email with correct body' do
+ receiver.execute
+
+ issue = Issue.last
+ expect(issue.description).to include('> This is an empty quote')
+ end
+ end
+
+ context 'when using custom service desk address' do
let(:receiver) { Gitlab::Email::ServiceDeskReceiver.new(email_raw) }
before do
stub_service_desk_email_setting(enabled: true, address: 'support+%{key}@example.com')
end
- before_all do
- create(:service_desk_setting, project: project, project_key: service_desk_key)
- end
+ context 'when using project key' do
+ let_it_be(:service_desk_key) { 'mykey' }
- it_behaves_like 'a new issue request'
+ let(:email_raw) { service_desk_fixture('emails/service_desk_custom_address.eml') }
+
+ before_all do
+ create(:service_desk_setting, project: project, project_key: service_desk_key)
+ end
+
+ it_behaves_like 'a new issue request'
+
+ context 'when there is no project with the key' do
+ let(:email_raw) { service_desk_fixture('emails/service_desk_custom_address.eml', key: 'some_key') }
+
+ it 'bounces the email' do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound)
+ end
+ end
+
+ context 'when the project slug does not match' do
+ let(:email_raw) { service_desk_fixture('emails/service_desk_custom_address.eml', slug: 'some-slug') }
+
+ it 'bounces the email' do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound)
+ end
+ end
+
+ context 'when there are multiple projects with same key' do
+ let_it_be(:project_with_same_key) { create(:project, group: group, service_desk_enabled: true) }
+
+ let(:email_raw) { service_desk_fixture('emails/service_desk_custom_address.eml', slug: project_with_same_key.full_path_slug.to_s) }
- context 'when there is no project with the key' do
- let(:email_raw) { service_desk_fixture('emails/service_desk_custom_address.eml', key: 'some_key') }
+ before do
+ create(:service_desk_setting, project: project_with_same_key, project_key: service_desk_key)
+ end
- it 'bounces the email' do
- expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound)
+ it 'process email for project with matching slug' do
+ expect { receiver.execute }.to change { Issue.count }.by(1)
+ expect(Issue.last.project).to eq(project_with_same_key)
+ end
end
end
- context 'when the project slug does not match' do
- let(:email_raw) { service_desk_fixture('emails/service_desk_custom_address.eml', slug: 'some-slug') }
+ context 'when project key is not set' do
+ let(:email_raw) { email_fixture('emails/service_desk_custom_address_no_key.eml') }
- it 'bounces the email' do
- expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound)
+ before do
+ stub_service_desk_email_setting(enabled: true, address: 'support+%{key}@example.com')
end
+
+ it_behaves_like 'a new issue request'
end
+ end
+ end
- context 'when there are multiple projects with same key' do
- let_it_be(:project_with_same_key) { create(:project, group: group, service_desk_enabled: true) }
+ context 'when rate limiting is in effect', :freeze_time, :clean_gitlab_redis_rate_limiting do
+ let(:receiver) { Gitlab::Email::Receiver.new(email_raw) }
- let(:email_raw) { service_desk_fixture('emails/service_desk_custom_address.eml', slug: project_with_same_key.full_path_slug.to_s) }
+ subject { 2.times { receiver.execute } }
- before do
- create(:service_desk_setting, project: project_with_same_key, project_key: service_desk_key)
+ before do
+ stub_feature_flags(rate_limited_service_issues_create: true)
+ stub_application_setting(issues_create_limit: 1)
+ end
+
+ context 'when too many requests are sent by one user' do
+ it 'raises an error' do
+ expect { subject }.to raise_error(RateLimitedService::RateLimitedError)
+ end
+
+ it 'creates 1 issue' do
+ expect do
+ subject
+ rescue RateLimitedService::RateLimitedError
+ end.to change { Issue.count }.by(1)
+ end
+
+ context 'when requests are sent by different users' do
+ let(:email_raw_2) { email_fixture('emails/service_desk_forwarded.eml') }
+ let(:receiver2) { Gitlab::Email::Receiver.new(email_raw_2) }
+
+ subject do
+ receiver.execute
+ receiver2.execute
end
- it 'process email for project with matching slug' do
- expect { receiver.execute }.to change { Issue.count }.by(1)
- expect(Issue.last.project).to eq(project_with_same_key)
+ it 'creates 2 issues' do
+ expect { subject }.to change { Issue.count }.by(2)
end
end
end
- context 'when rate limiting is in effect' do
- it 'allows unlimited new issue creation' do
- stub_application_setting(issues_create_limit: 1)
- setup_attachment
+ context 'when limit is higher than sent emails' do
+ before do
+ stub_application_setting(issues_create_limit: 2)
+ end
- expect { 2.times { receiver.execute } }.to change { Issue.count }.by(2)
+ it 'creates 2 issues' do
+ expect { subject }.to change { Issue.count }.by(2)
end
end
end
@@ -323,6 +387,7 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do
end
context 'when the email is forwarded through an alias' do
+ let(:author_email) { 'jake.g@adventuretime.ooo' }
let(:email_raw) { email_fixture('emails/service_desk_forwarded.eml') }
it_behaves_like 'a new issue request'
diff --git a/spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb b/spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb
index 0a1f04ed793..352eb596cd9 100644
--- a/spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb
+++ b/spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe Gitlab::Email::Hook::SmimeSignatureInterceptor do
end
before do
- allow(Gitlab::X509::Certificate).to receive_messages(from_files: certificate)
+ allow(Gitlab::Email::Hook::SmimeSignatureInterceptor).to receive(:certificate).and_return(certificate)
Mail.register_interceptor(described_class)
mail.deliver_now
diff --git a/spec/lib/gitlab/email/message/in_product_marketing/base_spec.rb b/spec/lib/gitlab/email/message/in_product_marketing/base_spec.rb
index 277f1158f8b..0521123f1ef 100644
--- a/spec/lib/gitlab/email/message/in_product_marketing/base_spec.rb
+++ b/spec/lib/gitlab/email/message/in_product_marketing/base_spec.rb
@@ -82,4 +82,29 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Base do
it { is_expected.to include('This is email 1 of 3 in the Create series', Gitlab::Routing.url_helpers.profile_notifications_url) }
end
end
+
+ describe '#series?' do
+ using RSpec::Parameterized::TableSyntax
+
+ subject do
+ test_class = "Gitlab::Email::Message::InProductMarketing::#{track.to_s.classify}".constantize
+ test_class.new(group: group, user: user, series: series).series?
+ end
+
+ where(:track, :result) do
+ :create | true
+ :team_short | true
+ :trial_short | true
+ :admin_verify | true
+ :verify | true
+ :trial | true
+ :team | true
+ :experience | true
+ :invite_team | false
+ end
+
+ with_them do
+ it { is_expected.to eq result }
+ end
+ end
end
diff --git a/spec/lib/gitlab/email/message/in_product_marketing/experience_spec.rb b/spec/lib/gitlab/email/message/in_product_marketing/experience_spec.rb
index b742eff3f56..8cd2345822e 100644
--- a/spec/lib/gitlab/email/message/in_product_marketing/experience_spec.rb
+++ b/spec/lib/gitlab/email/message/in_product_marketing/experience_spec.rb
@@ -22,14 +22,36 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Experience do
expect(message.cta_text).to be_nil
end
- describe '#feedback_link' do
- let(:member_count) { 2 }
+ describe 'feedback URL' do
+ before do
+ allow(message).to receive(:onboarding_progress).and_return(1)
+ allow(message).to receive(:show_invite_link).and_return(true)
+ end
+
+ subject do
+ message.feedback_link(1)
+ end
+
+ it { is_expected.to start_with(Gitlab::Saas.com_url) }
+
+ context 'when in development' do
+ let(:root_url) { 'http://example.com' }
+
+ before do
+ allow(message).to receive(:root_url).and_return(root_url)
+ stub_rails_env('development')
+ end
+
+ it { is_expected.to start_with(root_url) }
+ end
+ end
+
+ describe 'feedback URL show_invite_link query param' do
let(:user_access) { GroupMember::DEVELOPER }
let(:preferred_language) { 'en' }
before do
allow(message).to receive(:onboarding_progress).and_return(1)
- allow(group).to receive(:member_count).and_return(member_count)
allow(group).to receive(:max_member_access_for_user).and_return(user_access)
allow(user).to receive(:preferred_language).and_return(preferred_language)
end
@@ -41,12 +63,6 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Experience do
it { is_expected.to eq('true') }
- context 'with only one member' do
- let(:member_count) { 1 }
-
- it { is_expected.to eq('false') }
- end
-
context 'with less than developer access' do
let(:user_access) { GroupMember::GUEST }
@@ -59,6 +75,41 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Experience do
it { is_expected.to eq('false') }
end
end
+
+ describe 'feedback URL show_incentive query param' do
+ let(:show_invite_link) { true }
+ let(:member_count) { 2 }
+ let(:query) do
+ uri = URI.parse(message.feedback_link(1))
+ Rack::Utils.parse_query(uri.query).with_indifferent_access
+ end
+
+ before do
+ allow(message).to receive(:onboarding_progress).and_return(1)
+ allow(message).to receive(:show_invite_link).and_return(show_invite_link)
+ allow(group).to receive(:member_count).and_return(member_count)
+ end
+
+ subject { query[:show_incentive] }
+
+ it { is_expected.to eq('true') }
+
+ context 'with only one member' do
+ let(:member_count) { 1 }
+
+ it "is not present" do
+ expect(query).not_to have_key(:show_incentive)
+ end
+ end
+
+ context 'show_invite_link is false' do
+ let(:show_invite_link) { false }
+
+ it "is not present" do
+ expect(query).not_to have_key(:show_incentive)
+ end
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/email/message/in_product_marketing/invite_team_spec.rb b/spec/lib/gitlab/email/message/in_product_marketing/invite_team_spec.rb
new file mode 100644
index 00000000000..8319560f594
--- /dev/null
+++ b/spec/lib/gitlab/email/message/in_product_marketing/invite_team_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Email::Message::InProductMarketing::InviteTeam do
+ let_it_be(:group) { build(:group) }
+ let_it_be(:user) { build(:user) }
+
+ let(:series) { 0 }
+
+ subject(:message) { described_class.new(group: group, user: user, series: series) }
+
+ describe 'initialize' do
+ context 'when series is valid' do
+ it 'does not raise error' do
+ expect { subject }.not_to raise_error(ArgumentError)
+ end
+ end
+
+ context 'when series is invalid' do
+ let(:series) { 1 }
+
+ it 'raises error' do
+ expect { subject }.to raise_error(ArgumentError)
+ end
+ end
+ end
+
+ it 'contains the correct message', :aggregate_failures do
+ expect(message.subject_line).to eq 'Invite your teammates to GitLab'
+ expect(message.tagline).to be_empty
+ expect(message.title).to eq 'GitLab is better with teammates to help out!'
+ expect(message.subtitle).to be_empty
+ expect(message.body_line1).to eq 'Invite your teammates today and build better code together. You can even assign tasks to new teammates such as setting up CI/CD, to help get projects up and running.'
+ expect(message.body_line2).to be_empty
+ expect(message.cta_text).to eq 'Invite your teammates to help'
+ expect(message.logo_path).to eq 'mailers/in_product_marketing/team-0.png'
+ end
+end
diff --git a/spec/lib/gitlab/email/message/in_product_marketing_spec.rb b/spec/lib/gitlab/email/message/in_product_marketing_spec.rb
index 9ffc4a340a3..594df7440bb 100644
--- a/spec/lib/gitlab/email/message/in_product_marketing_spec.rb
+++ b/spec/lib/gitlab/email/message/in_product_marketing_spec.rb
@@ -10,10 +10,15 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing do
context 'when track exists' do
where(:track, :expected_class) do
- :create | described_class::Create
- :verify | described_class::Verify
- :trial | described_class::Trial
- :team | described_class::Team
+ :create | described_class::Create
+ :team_short | described_class::TeamShort
+ :trial_short | described_class::TrialShort
+ :admin_verify | described_class::AdminVerify
+ :verify | described_class::Verify
+ :trial | described_class::Trial
+ :team | described_class::Team
+ :experience | described_class::Experience
+ :invite_team | described_class::InviteTeam
end
with_them do
diff --git a/spec/lib/gitlab/email/reply_parser_spec.rb b/spec/lib/gitlab/email/reply_parser_spec.rb
index 3b01b568fb4..c0d177aff4d 100644
--- a/spec/lib/gitlab/email/reply_parser_spec.rb
+++ b/spec/lib/gitlab/email/reply_parser_spec.rb
@@ -21,6 +21,30 @@ RSpec.describe Gitlab::Email::ReplyParser do
expect(test_parse_body(fixture_file("emails/no_content_reply.eml"))).to eq("")
end
+ context 'when allow_only_quotes is true' do
+ it "returns quoted text from email" do
+ text = test_parse_body(fixture_file("emails/no_content_reply.eml"), allow_only_quotes: true)
+
+ expect(text).to eq(
+ <<-BODY.strip_heredoc.chomp
+ >
+ >
+ >
+ > eviltrout posted in 'Adventure Time Sux' on Discourse Meta:
+ >
+ > ---
+ > hey guys everyone knows adventure time sucks!
+ >
+ > ---
+ > Please visit this link to respond: http://localhost:3000/t/adventure-time-sux/1234/3
+ >
+ > To unsubscribe from these emails, visit your [user preferences](http://localhost:3000/user_preferences).
+ >
+ BODY
+ )
+ end
+ end
+
it "properly renders plaintext-only email" do
expect(test_parse_body(fixture_file("emails/plaintext_only.eml")))
.to eq(
diff --git a/spec/lib/gitlab/emoji_spec.rb b/spec/lib/gitlab/emoji_spec.rb
index 8f855489c12..0db3b5f3b11 100644
--- a/spec/lib/gitlab/emoji_spec.rb
+++ b/spec/lib/gitlab/emoji_spec.rb
@@ -3,90 +3,6 @@
require 'spec_helper'
RSpec.describe Gitlab::Emoji do
- let_it_be(:emojis) { Gemojione.index.instance_variable_get(:@emoji_by_name) }
- let_it_be(:emojis_by_moji) { Gemojione.index.instance_variable_get(:@emoji_by_moji) }
- let_it_be(:emoji_unicode_versions_by_name) { Gitlab::Json.parse(File.read(Rails.root.join('fixtures', 'emojis', 'emoji-unicode-version-map.json'))) }
- let_it_be(:emojis_aliases) { Gitlab::Json.parse(File.read(Rails.root.join('fixtures', 'emojis', 'aliases.json'))) }
-
- describe '.emojis' do
- it 'returns emojis' do
- current_emojis = described_class.emojis
-
- expect(current_emojis).to eq(emojis)
- end
- end
-
- describe '.emojis_by_moji' do
- it 'return emojis by moji' do
- current_emojis_by_moji = described_class.emojis_by_moji
-
- expect(current_emojis_by_moji).to eq(emojis_by_moji)
- end
- end
-
- describe '.emojis_unicodes' do
- it 'returns emoji unicodes' do
- emoji_keys = described_class.emojis_unicodes
-
- expect(emoji_keys).to eq(emojis_by_moji.keys)
- end
- end
-
- describe '.emojis_names' do
- it 'returns emoji names' do
- emoji_names = described_class.emojis_names
-
- expect(emoji_names).to eq(emojis.keys)
- end
- end
-
- describe '.emojis_aliases' do
- it 'returns emoji aliases' do
- emoji_aliases = described_class.emojis_aliases
-
- expect(emoji_aliases).to eq(emojis_aliases)
- end
- end
-
- describe '.emoji_filename' do
- it 'returns emoji filename' do
- # "100" => {"unicode"=>"1F4AF"...}
- emoji_filename = described_class.emoji_filename('100')
-
- expect(emoji_filename).to eq(emojis['100']['unicode'])
- end
- end
-
- describe '.emoji_unicode_filename' do
- it 'returns emoji unicode filename' do
- emoji_unicode_filename = described_class.emoji_unicode_filename('💯')
-
- expect(emoji_unicode_filename).to eq(emojis_by_moji['💯']['unicode'])
- end
- end
-
- describe '.emoji_unicode_version' do
- it 'returns emoji unicode version by name' do
- emoji_unicode_version = described_class.emoji_unicode_version('100')
-
- expect(emoji_unicode_version).to eq(emoji_unicode_versions_by_name['100'])
- end
- end
-
- describe '.normalize_emoji_name' do
- it 'returns same name if not found in aliases' do
- emoji_name = described_class.normalize_emoji_name('random')
-
- expect(emoji_name).to eq('random')
- end
-
- it 'returns name if name found in aliases' do
- emoji_name = described_class.normalize_emoji_name('small_airplane')
-
- expect(emoji_name).to eq(emojis_aliases['small_airplane'])
- end
- end
-
describe '.emoji_image_tag' do
it 'returns emoji image tag' do
emoji_image = described_class.emoji_image_tag('emoji_one', 'src_url')
@@ -104,29 +20,17 @@ RSpec.describe Gitlab::Emoji do
end
end
- describe '.emoji_exists?' do
- it 'returns true if the name exists' do
- emoji_exists = described_class.emoji_exists?('100')
-
- expect(emoji_exists).to be_truthy
- end
-
- it 'returns false if the name does not exist' do
- emoji_exists = described_class.emoji_exists?('random')
-
- expect(emoji_exists).to be_falsey
- end
- end
-
describe '.gl_emoji_tag' do
it 'returns gl emoji tag if emoji is found' do
- gl_tag = described_class.gl_emoji_tag('small_airplane')
+ emoji = TanukiEmoji.find_by_alpha_code('small_airplane')
+ gl_tag = described_class.gl_emoji_tag(emoji)
expect(gl_tag).to eq('<gl-emoji title="small airplane" data-name="airplane_small" data-unicode-version="7.0">🛩</gl-emoji>')
end
- it 'returns nil if emoji name is not found' do
- gl_tag = described_class.gl_emoji_tag('random')
+ it 'returns nil if emoji is not found' do
+ emoji = TanukiEmoji.find_by_alpha_code('random')
+ gl_tag = described_class.gl_emoji_tag(emoji)
expect(gl_tag).to be_nil
end
diff --git a/spec/lib/gitlab/etag_caching/middleware_spec.rb b/spec/lib/gitlab/etag_caching/middleware_spec.rb
index c4da89e5f5c..982c0d911bc 100644
--- a/spec/lib/gitlab/etag_caching/middleware_spec.rb
+++ b/spec/lib/gitlab/etag_caching/middleware_spec.rb
@@ -174,7 +174,7 @@ RSpec.describe Gitlab::EtagCaching::Middleware, :clean_gitlab_redis_shared_state
it "pushes route's feature category to the context" do
expect(Gitlab::ApplicationContext).to receive(:push).with(
- feature_category: 'issue_tracking'
+ feature_category: 'team_planning'
)
_, _, _ = middleware.call(build_request(path, if_none_match))
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index f4dba5e8d58..11510daf9c0 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -715,6 +715,14 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do
it { is_expected.not_to include("feature") }
end
+ describe '#first_ref_by_oid' do
+ let(:commit) { described_class.find(repository, 'master') }
+
+ subject { commit.first_ref_by_oid(repository) }
+
+ it { is_expected.to eq("master") }
+ end
+
describe '.get_message' do
let(:commit_ids) { %w[6d394385cf567f80a8fd85055db1ab4c5295806f cfe32cf61b73a0d5e9f13e774abde7ff789b1660] }
diff --git a/spec/lib/gitlab/git/object_pool_spec.rb b/spec/lib/gitlab/git/object_pool_spec.rb
index e1873c6ddb5..91960ebbede 100644
--- a/spec/lib/gitlab/git/object_pool_spec.rb
+++ b/spec/lib/gitlab/git/object_pool_spec.rb
@@ -112,7 +112,7 @@ RSpec.describe Gitlab::Git::ObjectPool do
subject.fetch
- expect(subject.repository.commit_count('refs/remotes/origin/master')).to eq(commit_count)
+ expect(subject.repository.commit_count('refs/remotes/origin/heads/master')).to eq(commit_count)
expect(subject.repository.commit(new_commit_id).id).to eq(new_commit_id)
end
end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index c7b68ff3e28..f1b6a59abf9 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -125,7 +125,22 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
it 'gets tags from GitalyClient' do
expect_next_instance_of(Gitlab::GitalyClient::RefService) do |service|
- expect(service).to receive(:tags).with(sort_by: 'name_asc')
+ expect(service).to receive(:tags).with(sort_by: 'name_asc', pagination_params: nil)
+ end
+
+ subject
+ end
+ end
+
+ context 'with pagination option' do
+ subject { repository.tags(pagination_params: { limit: 5, page_token: 'refs/tags/v1.0.0' }) }
+
+ it 'gets tags from GitalyClient' do
+ expect_next_instance_of(Gitlab::GitalyClient::RefService) do |service|
+ expect(service).to receive(:tags).with(
+ sort_by: nil,
+ pagination_params: { limit: 5, page_token: 'refs/tags/v1.0.0' }
+ )
end
subject
@@ -1888,6 +1903,44 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
end
end
+ describe '#list_refs' do
+ it 'returns a list of branches with their head commit' do
+ refs = repository.list_refs
+ reference = refs.first
+
+ expect(refs).to be_an(Enumerable)
+ expect(reference).to be_a(Gitaly::ListRefsResponse::Reference)
+ expect(reference.name).to be_a(String)
+ expect(reference.target).to be_a(String)
+ end
+ end
+
+ describe '#refs_by_oid' do
+ it 'returns a list of refs from a OID' do
+ refs = repository.refs_by_oid(oid: repository.commit.id)
+
+ expect(refs).to be_an(Array)
+ expect(refs).to include(Gitlab::Git::BRANCH_REF_PREFIX + repository.root_ref)
+ end
+
+ it 'returns a single ref from a OID' do
+ refs = repository.refs_by_oid(oid: repository.commit.id, limit: 1)
+
+ expect(refs).to be_an(Array)
+ expect(refs).to eq([Gitlab::Git::BRANCH_REF_PREFIX + repository.root_ref])
+ end
+
+ it 'returns empty for unknown ID' do
+ expect(repository.refs_by_oid(oid: Gitlab::Git::BLANK_SHA, limit: 0)).to eq([])
+ end
+
+ it 'returns nil for an empty repo' do
+ project = create(:project)
+
+ expect(project.repository.refs_by_oid(oid: SeedRepo::Commit::ID, limit: 0)).to be_nil
+ end
+ end
+
describe '#set_full_path' do
before do
repository_rugged.config["gitlab.fullpath"] = repository_path
diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
index 554a91f2bc5..d8e397dd6f3 100644
--- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
@@ -112,15 +112,38 @@ RSpec.describe Gitlab::GitalyClient::CommitService do
let(:from) { 'master' }
let(:to) { Gitlab::Git::EMPTY_TREE_ID }
- it 'sends an RPC request' do
- request = Gitaly::CommitsBetweenRequest.new(
- repository: repository_message, from: from, to: to
- )
+ context 'with between_commits_via_list_commits enabled' do
+ before do
+ stub_feature_flags(between_commits_via_list_commits: true)
+ end
- expect_any_instance_of(Gitaly::CommitService::Stub).to receive(:commits_between)
- .with(request, kind_of(Hash)).and_return([])
+ it 'sends an RPC request' do
+ request = Gitaly::ListCommitsRequest.new(
+ repository: repository_message, revisions: ["^" + from, to], reverse: true
+ )
+
+ expect_any_instance_of(Gitaly::CommitService::Stub).to receive(:list_commits)
+ .with(request, kind_of(Hash)).and_return([])
- described_class.new(repository).between(from, to)
+ described_class.new(repository).between(from, to)
+ end
+ end
+
+ context 'with between_commits_via_list_commits disabled' do
+ before do
+ stub_feature_flags(between_commits_via_list_commits: false)
+ end
+
+ it 'sends an RPC request' do
+ request = Gitaly::CommitsBetweenRequest.new(
+ repository: repository_message, from: from, to: to
+ )
+
+ expect_any_instance_of(Gitaly::CommitService::Stub).to receive(:commits_between)
+ .with(request, kind_of(Hash)).and_return([])
+
+ described_class.new(repository).between(from, to)
+ end
end
end
diff --git a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
index d308612ef31..2e37c98a591 100644
--- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
@@ -190,6 +190,22 @@ RSpec.describe Gitlab::GitalyClient::RefService do
client.tags(sort_by: 'name_asc')
end
end
+
+ context 'with pagination option' do
+ it 'sends a correct find_all_tags message' do
+ expected_pagination = Gitaly::PaginationParameter.new(
+ limit: 5,
+ page_token: 'refs/tags/v1.0.0'
+ )
+
+ expect_any_instance_of(Gitaly::RefService::Stub)
+ .to receive(:find_all_tags)
+ .with(gitaly_request_with_params(pagination_params: expected_pagination), kind_of(Hash))
+ .and_return([])
+
+ client.tags(pagination_params: { limit: 5, page_token: 'refs/tags/v1.0.0' })
+ end
+ end
end
describe '#branch_names_contains_sha' do
@@ -252,6 +268,26 @@ RSpec.describe Gitlab::GitalyClient::RefService do
end
end
+ describe '#list_refs' do
+ it 'sends a list_refs message' do
+ expect_any_instance_of(Gitaly::RefService::Stub)
+ .to receive(:list_refs)
+ .with(gitaly_request_with_params(patterns: ['refs/heads/']), kind_of(Hash))
+ .and_call_original
+
+ client.list_refs
+ end
+
+ it 'accepts a patterns argument' do
+ expect_any_instance_of(Gitaly::RefService::Stub)
+ .to receive(:list_refs)
+ .with(gitaly_request_with_params(patterns: ['refs/tags/']), kind_of(Hash))
+ .and_call_original
+
+ client.list_refs([Gitlab::Git::TAG_REF_PREFIX])
+ end
+ end
+
describe '#pack_refs' do
it 'sends a pack_refs message' do
expect_any_instance_of(Gitaly::RefService::Stub)
@@ -262,4 +298,19 @@ RSpec.describe Gitlab::GitalyClient::RefService do
client.pack_refs
end
end
+
+ describe '#find_refs_by_oid' do
+ let(:oid) { project.repository.commit.id }
+
+ it 'sends a find_refs_by_oid message' do
+ expect_any_instance_of(Gitaly::RefService::Stub)
+ .to receive(:find_refs_by_oid)
+ .with(gitaly_request_with_params(sort_field: 'refname', oid: oid, limit: 1), kind_of(Hash))
+ .and_call_original
+
+ refs = client.find_refs_by_oid(oid: oid, limit: 1)
+
+ expect(refs.to_a).to eq([Gitlab::Git::BRANCH_REF_PREFIX + project.repository.root_ref])
+ end
+ end
end
diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb
index 16f75691288..ba4ea1069d8 100644
--- a/spec/lib/gitlab/gitaly_client_spec.rb
+++ b/spec/lib/gitlab/gitaly_client_spec.rb
@@ -5,14 +5,6 @@ require 'spec_helper'
# We stub Gitaly in `spec/support/gitaly.rb` for other tests. We don't want
# those stubs while testing the GitalyClient itself.
RSpec.describe Gitlab::GitalyClient do
- let(:sample_cert) { Rails.root.join('spec/fixtures/clusters/sample_cert.pem').to_s }
-
- before do
- allow(described_class)
- .to receive(:stub_cert_paths)
- .and_return([sample_cert])
- end
-
def stub_repos_storages(address)
allow(Gitlab.config.repositories).to receive(:storages).and_return({
'default' => { 'gitaly_address' => address }
@@ -142,21 +134,6 @@ RSpec.describe Gitlab::GitalyClient do
end
end
- describe '.stub_certs' do
- it 'skips certificates if OpenSSLError is raised and report it' do
- expect(Gitlab::ErrorTracking)
- .to receive(:track_and_raise_for_dev_exception)
- .with(
- a_kind_of(OpenSSL::X509::CertificateError),
- cert_file: a_kind_of(String)).at_least(:once)
-
- expect(OpenSSL::X509::Certificate)
- .to receive(:new)
- .and_raise(OpenSSL::X509::CertificateError).at_least(:once)
-
- expect(described_class.stub_certs).to be_a(String)
- end
- end
describe '.stub_creds' do
it 'returns :this_channel_is_insecure if unix' do
address = 'unix:/tmp/gitaly.sock'
diff --git a/spec/lib/gitlab/github_import/bulk_importing_spec.rb b/spec/lib/gitlab/github_import/bulk_importing_spec.rb
index 6c94973b5a8..e170496ff7b 100644
--- a/spec/lib/gitlab/github_import/bulk_importing_spec.rb
+++ b/spec/lib/gitlab/github_import/bulk_importing_spec.rb
@@ -116,13 +116,13 @@ RSpec.describe Gitlab::GithubImport::BulkImporting do
value: 5
)
- expect(Gitlab::Database.main)
- .to receive(:bulk_insert)
+ expect(ApplicationRecord)
+ .to receive(:legacy_bulk_insert)
.ordered
.with('kittens', rows.first(5))
- expect(Gitlab::Database.main)
- .to receive(:bulk_insert)
+ expect(ApplicationRecord)
+ .to receive(:legacy_bulk_insert)
.ordered
.with('kittens', rows.last(5))
diff --git a/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb b/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb
index 3dc15c7c059..0448ada6bca 100644
--- a/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb
@@ -2,156 +2,226 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Importer::DiffNoteImporter do
- let(:project) { create(:project) }
- let(:client) { double(:client) }
- let(:user) { create(:user) }
- let(:created_at) { Time.new(2017, 1, 1, 12, 00) }
- let(:updated_at) { Time.new(2017, 1, 1, 12, 15) }
+RSpec.describe Gitlab::GithubImport::Importer::DiffNoteImporter, :aggregate_failures do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
- let(:hunk) do
- '@@ -1 +1 @@
+ let(:client) { double(:client) }
+ let(:discussion_id) { 'b0fa404393eeebb4e82becb8104f238812bb1fe6' }
+ let(:created_at) { Time.new(2017, 1, 1, 12, 00).utc }
+ let(:updated_at) { Time.new(2017, 1, 1, 12, 15).utc }
+ let(:note_body) { 'Hello' }
+ let(:file_path) { 'files/ruby/popen.rb' }
+
+ let(:diff_hunk) do
+ '@@ -14 +14 @@
-Hello
+Hello world'
end
- let(:note) do
+ let(:note_representation) do
Gitlab::GithubImport::Representation::DiffNote.new(
noteable_type: 'MergeRequest',
noteable_id: 1,
commit_id: '123abc',
original_commit_id: 'original123abc',
- file_path: 'README.md',
- diff_hunk: hunk,
- author: Gitlab::GithubImport::Representation::User
- .new(id: user.id, login: user.username),
- note: 'Hello',
+ file_path: file_path,
+ author: Gitlab::GithubImport::Representation::User.new(id: user.id, login: user.username),
+ note: note_body,
created_at: created_at,
updated_at: updated_at,
- github_id: 1
+ start_line: nil,
+ end_line: 15,
+ github_id: 1,
+ diff_hunk: diff_hunk,
+ side: 'RIGHT'
)
end
- let(:importer) { described_class.new(note, project, client) }
+ subject(:importer) { described_class.new(note_representation, project, client) }
+
+ shared_examples 'diff notes without suggestion' do
+ it 'imports the note as legacy diff note' do
+ stub_user_finder(user.id, true)
+
+ expect { subject.execute }
+ .to change(LegacyDiffNote, :count)
+ .by(1)
+
+ note = project.notes.diff_notes.take
+ expect(note).to be_valid
+ expect(note.author_id).to eq(user.id)
+ expect(note.commit_id).to eq('original123abc')
+ expect(note.created_at).to eq(created_at)
+ expect(note.diff).to be_an_instance_of(Gitlab::Git::Diff)
+ expect(note.discussion_id).to eq(discussion_id)
+ expect(note.line_code).to eq(note_representation.line_code)
+ expect(note.note).to eq('Hello')
+ expect(note.noteable_id).to eq(merge_request.id)
+ expect(note.noteable_type).to eq('MergeRequest')
+ expect(note.project_id).to eq(project.id)
+ expect(note.st_diff).to eq(note_representation.diff_hash)
+ expect(note.system).to eq(false)
+ expect(note.type).to eq('LegacyDiffNote')
+ expect(note.updated_at).to eq(updated_at)
+ end
+
+ it 'adds a "created by:" note when the author cannot be found' do
+ stub_user_finder(project.creator_id, false)
+
+ expect { subject.execute }
+ .to change(LegacyDiffNote, :count)
+ .by(1)
+
+ note = project.notes.diff_notes.take
+ expect(note).to be_valid
+ expect(note.author_id).to eq(project.creator_id)
+ expect(note.note).to eq("*Created by: #{user.username}*\n\nHello")
+ end
+
+ it 'does not import the note when a foreign key error is raised' do
+ stub_user_finder(project.creator_id, false)
+
+ expect(ApplicationRecord)
+ .to receive(:legacy_bulk_insert)
+ .and_raise(ActiveRecord::InvalidForeignKey, 'invalid foreign key')
+
+ expect { subject.execute }
+ .not_to change(LegacyDiffNote, :count)
+ end
+ end
describe '#execute' do
context 'when the merge request no longer exists' do
it 'does not import anything' do
- expect(Gitlab::Database.main).not_to receive(:bulk_insert)
+ expect(ApplicationRecord).not_to receive(:legacy_bulk_insert)
- importer.execute
+ expect { subject.execute }
+ .to not_change(DiffNote, :count)
+ .and not_change(LegacyDiffNote, :count)
end
end
context 'when the merge request exists' do
- let!(:merge_request) do
+ let_it_be(:merge_request) do
create(:merge_request, source_project: project, target_project: project)
end
before do
- allow(importer)
- .to receive(:find_merge_request_id)
- .and_return(merge_request.id)
+ expect_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder|
+ expect(finder)
+ .to receive(:database_id)
+ .and_return(merge_request.id)
+ end
+
+ expect(Discussion)
+ .to receive(:discussion_id)
+ .and_return(discussion_id)
end
- it 'imports the note' do
- allow(importer.user_finder)
- .to receive(:author_id_for)
- .and_return([user.id, true])
-
- expect(Gitlab::Database.main)
- .to receive(:bulk_insert)
- .with(
- LegacyDiffNote.table_name,
- [
- {
- discussion_id: anything,
- noteable_type: 'MergeRequest',
- noteable_id: merge_request.id,
- project_id: project.id,
- author_id: user.id,
- note: 'Hello',
- system: false,
- commit_id: 'original123abc',
- line_code: note.line_code,
- type: 'LegacyDiffNote',
- created_at: created_at,
- updated_at: updated_at,
- st_diff: note.diff_hash.to_yaml
- }
- ]
- )
- .and_call_original
-
- importer.execute
+ context 'when github_importer_use_diff_note_with_suggestions is disabled' do
+ before do
+ stub_feature_flags(github_importer_use_diff_note_with_suggestions: false)
+ end
+
+ it_behaves_like 'diff notes without suggestion'
+
+ context 'when the note has suggestions' do
+ let(:note_body) do
+ <<~EOB
+ Suggestion:
+ ```suggestion
+ what do you think to do it like this
+ ```
+ EOB
+ end
+
+ it 'imports the note' do
+ stub_user_finder(user.id, true)
+
+ expect { subject.execute }
+ .to change(LegacyDiffNote, :count)
+ .and not_change(DiffNote, :count)
+
+ note = project.notes.diff_notes.take
+ expect(note).to be_valid
+ expect(note.note)
+ .to eq <<~NOTE
+ Suggestion:
+ ```suggestion:-0+0
+ what do you think to do it like this
+ ```
+ NOTE
+ end
+ end
end
- it 'imports the note when the author could not be found' do
- allow(importer.user_finder)
- .to receive(:author_id_for)
- .and_return([project.creator_id, false])
-
- expect(Gitlab::Database.main)
- .to receive(:bulk_insert)
- .with(
- LegacyDiffNote.table_name,
- [
- {
- discussion_id: anything,
- noteable_type: 'MergeRequest',
- noteable_id: merge_request.id,
- project_id: project.id,
- author_id: project.creator_id,
- note: "*Created by: #{user.username}*\n\nHello",
- system: false,
- commit_id: 'original123abc',
- line_code: note.line_code,
- type: 'LegacyDiffNote',
- created_at: created_at,
- updated_at: updated_at,
- st_diff: note.diff_hash.to_yaml
- }
- ]
- )
- .and_call_original
-
- importer.execute
- end
-
- it 'produces a valid LegacyDiffNote' do
- allow(importer.user_finder)
- .to receive(:author_id_for)
- .and_return([user.id, true])
-
- importer.execute
-
- note = project.notes.diff_notes.take
-
- expect(note).to be_valid
- expect(note.diff).to be_an_instance_of(Gitlab::Git::Diff)
- end
-
- it 'does not import the note when a foreign key error is raised' do
- allow(importer.user_finder)
- .to receive(:author_id_for)
- .and_return([project.creator_id, false])
-
- expect(Gitlab::Database.main)
- .to receive(:bulk_insert)
- .and_raise(ActiveRecord::InvalidForeignKey, 'invalid foreign key')
-
- expect { importer.execute }.not_to raise_error
+ context 'when github_importer_use_diff_note_with_suggestions is enabled' do
+ before do
+ stub_feature_flags(github_importer_use_diff_note_with_suggestions: true)
+ end
+
+ it_behaves_like 'diff notes without suggestion'
+
+ context 'when the note has suggestions' do
+ let(:note_body) do
+ <<~EOB
+ Suggestion:
+ ```suggestion
+ what do you think to do it like this
+ ```
+ EOB
+ end
+
+ it 'imports the note as diff note' do
+ stub_user_finder(user.id, true)
+
+ expect { subject.execute }
+ .to change(DiffNote, :count)
+ .by(1)
+
+ note = project.notes.diff_notes.take
+ expect(note).to be_valid
+ expect(note.noteable_type).to eq('MergeRequest')
+ expect(note.noteable_id).to eq(merge_request.id)
+ expect(note.project_id).to eq(project.id)
+ expect(note.author_id).to eq(user.id)
+ expect(note.system).to eq(false)
+ expect(note.discussion_id).to eq(discussion_id)
+ expect(note.commit_id).to eq('original123abc')
+ expect(note.line_code).to eq(note_representation.line_code)
+ expect(note.type).to eq('DiffNote')
+ expect(note.created_at).to eq(created_at)
+ expect(note.updated_at).to eq(updated_at)
+ expect(note.position.to_h).to eq({
+ base_sha: merge_request.diffs.diff_refs.base_sha,
+ head_sha: merge_request.diffs.diff_refs.head_sha,
+ start_sha: merge_request.diffs.diff_refs.start_sha,
+ new_line: 15,
+ old_line: nil,
+ new_path: file_path,
+ old_path: file_path,
+ position_type: 'text',
+ line_range: nil
+ })
+ expect(note.note)
+ .to eq <<~NOTE
+ Suggestion:
+ ```suggestion:-0+0
+ what do you think to do it like this
+ ```
+ NOTE
+ end
+ end
end
end
end
- describe '#find_merge_request_id' do
- it 'returns a merge request ID' do
- expect_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |instance|
- expect(instance).to receive(:database_id).and_return(10)
- end
-
- expect(importer.find_merge_request_id).to eq(10)
+ def stub_user_finder(user, found)
+ expect_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder|
+ expect(finder)
+ .to receive(:author_id_for)
+ .and_return([user, found])
end
end
end
diff --git a/spec/lib/gitlab/github_import/importer/diff_notes_importer_spec.rb b/spec/lib/gitlab/github_import/importer/diff_notes_importer_spec.rb
index be4fc3cbf16..1c7b35ed928 100644
--- a/spec/lib/gitlab/github_import/importer/diff_notes_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/diff_notes_importer_spec.rb
@@ -19,7 +19,9 @@ RSpec.describe Gitlab::GithubImport::Importer::DiffNotesImporter do
updated_at: Time.zone.now,
line: 23,
start_line: nil,
+ in_reply_to_id: nil,
id: 1,
+ side: 'RIGHT',
body: <<~BODY
Hello World
diff --git a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb
index 0926000428c..4287c32b947 100644
--- a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb
@@ -190,8 +190,8 @@ RSpec.describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redi
.with(issue.assignees[1])
.and_return(5)
- expect(Gitlab::Database.main)
- .to receive(:bulk_insert)
+ expect(ApplicationRecord)
+ .to receive(:legacy_bulk_insert)
.with(
IssueAssignee.table_name,
[{ issue_id: 1, user_id: 4 }, { issue_id: 1, user_id: 5 }]
diff --git a/spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb b/spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb
index 241a0fef600..e68849755b2 100644
--- a/spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb
@@ -39,8 +39,8 @@ RSpec.describe Gitlab::GithubImport::Importer::LabelLinksImporter do
.and_return(1)
freeze_time do
- expect(Gitlab::Database.main)
- .to receive(:bulk_insert)
+ expect(ApplicationRecord)
+ .to receive(:legacy_bulk_insert)
.with(
LabelLink.table_name,
[
@@ -64,8 +64,8 @@ RSpec.describe Gitlab::GithubImport::Importer::LabelLinksImporter do
.with('bug')
.and_return(nil)
- expect(Gitlab::Database.main)
- .to receive(:bulk_insert)
+ expect(ApplicationRecord)
+ .to receive(:legacy_bulk_insert)
.with(LabelLink.table_name, [])
importer.create_labels
diff --git a/spec/lib/gitlab/github_import/importer/note_importer_spec.rb b/spec/lib/gitlab/github_import/importer/note_importer_spec.rb
index 820f46c7286..96d8acbd3de 100644
--- a/spec/lib/gitlab/github_import/importer/note_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/note_importer_spec.rb
@@ -41,8 +41,8 @@ RSpec.describe Gitlab::GithubImport::Importer::NoteImporter do
.with(github_note)
.and_return([user.id, true])
- expect(Gitlab::Database.main)
- .to receive(:bulk_insert)
+ expect(ApplicationRecord)
+ .to receive(:legacy_bulk_insert)
.with(
Note.table_name,
[
@@ -71,8 +71,8 @@ RSpec.describe Gitlab::GithubImport::Importer::NoteImporter do
.with(github_note)
.and_return([project.creator_id, false])
- expect(Gitlab::Database.main)
- .to receive(:bulk_insert)
+ expect(ApplicationRecord)
+ .to receive(:legacy_bulk_insert)
.with(
Note.table_name,
[
@@ -115,7 +115,7 @@ RSpec.describe Gitlab::GithubImport::Importer::NoteImporter do
context 'when the noteable does not exist' do
it 'does not import the note' do
- expect(Gitlab::Database.main).not_to receive(:bulk_insert)
+ expect(ApplicationRecord).not_to receive(:legacy_bulk_insert)
importer.execute
end
@@ -134,8 +134,8 @@ RSpec.describe Gitlab::GithubImport::Importer::NoteImporter do
.with(github_note)
.and_return([user.id, true])
- expect(Gitlab::Database.main)
- .to receive(:bulk_insert)
+ expect(ApplicationRecord)
+ .to receive(:legacy_bulk_insert)
.and_raise(ActiveRecord::InvalidForeignKey, 'invalid foreign key')
expect { importer.execute }.not_to raise_error
diff --git a/spec/lib/gitlab/github_import/importer/pull_requests_merged_by_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests_merged_by_importer_spec.rb
index 4a47d103cde..b6c162aafa9 100644
--- a/spec/lib/gitlab/github_import/importer/pull_requests_merged_by_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_requests_merged_by_importer_spec.rb
@@ -4,7 +4,8 @@ require 'spec_helper'
RSpec.describe Gitlab::GithubImport::Importer::PullRequestsMergedByImporter do
let(:client) { double }
- let(:project) { create(:project, import_source: 'http://somegithub.com') }
+
+ let_it_be(:project) { create(:project, import_source: 'http://somegithub.com') }
subject { described_class.new(project, client) }
@@ -27,14 +28,11 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestsMergedByImporter do
end
describe '#each_object_to_import', :clean_gitlab_redis_cache do
- it 'fetchs the merged pull requests data' do
- create(
- :merged_merge_request,
- iid: 999,
- source_project: project,
- target_project: project
- )
+ let!(:merge_request) do
+ create(:merged_merge_request, iid: 999, source_project: project, target_project: project)
+ end
+ it 'fetches the merged pull requests data' do
pull_request = double
allow(client)
@@ -48,5 +46,16 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestsMergedByImporter do
subject.each_object_to_import {}
end
+
+ it 'skips cached merge requests' do
+ Gitlab::Cache::Import::Caching.set_add(
+ "github-importer/already-imported/#{project.id}/pull_requests_merged_by",
+ merge_request.id
+ )
+
+ expect(client).not_to receive(:pull_request)
+
+ subject.each_object_to_import {}
+ end
end
end
diff --git a/spec/lib/gitlab/github_import/representation/diff_note_spec.rb b/spec/lib/gitlab/github_import/representation/diff_note_spec.rb
index 81722c0eba7..63834cfdb94 100644
--- a/spec/lib/gitlab/github_import/representation/diff_note_spec.rb
+++ b/spec/lib/gitlab/github_import/representation/diff_note_spec.rb
@@ -2,23 +2,44 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Representation::DiffNote do
+RSpec.describe Gitlab::GithubImport::Representation::DiffNote, :clean_gitlab_redis_shared_state do
let(:hunk) do
'@@ -1 +1 @@
-Hello
+Hello world'
end
+ let(:merge_request) do
+ double(
+ :merge_request,
+ id: 54,
+ diff_refs: double(
+ :refs,
+ base_sha: 'base',
+ start_sha: 'start',
+ head_sha: 'head'
+ )
+ )
+ end
+
+ let(:project) { double(:project, id: 836) }
+ let(:note_id) { 1 }
+ let(:in_reply_to_id) { nil }
+ let(:start_line) { nil }
+ let(:end_line) { 23 }
+ let(:note_body) { 'Hello world' }
+ let(:user_data) { { 'id' => 4, 'login' => 'alice' } }
+ let(:side) { 'RIGHT' }
let(:created_at) { Time.new(2017, 1, 1, 12, 00) }
let(:updated_at) { Time.new(2017, 1, 1, 12, 15) }
- shared_examples 'a DiffNote' do
+ shared_examples 'a DiffNote representation' do
it 'returns an instance of DiffNote' do
expect(note).to be_an_instance_of(described_class)
end
context 'the returned DiffNote' do
- it 'includes the number of the note' do
+ it 'includes the number of the merge request' do
expect(note.noteable_id).to eq(42)
end
@@ -30,18 +51,6 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNote do
expect(note.commit_id).to eq('123abc')
end
- it 'includes the user details' do
- expect(note.author)
- .to be_an_instance_of(Gitlab::GithubImport::Representation::User)
-
- expect(note.author.id).to eq(4)
- expect(note.author.login).to eq('alice')
- end
-
- it 'includes the note body' do
- expect(note.note).to eq('Hello world')
- end
-
it 'includes the created timestamp' do
expect(note.created_at).to eq(created_at)
end
@@ -51,209 +60,250 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNote do
end
it 'includes the GitHub ID' do
- expect(note.note_id).to eq(1)
+ expect(note.note_id).to eq(note_id)
end
it 'returns the noteable type' do
expect(note.noteable_type).to eq('MergeRequest')
end
- end
- end
-
- describe '.from_api_response' do
- let(:response) do
- double(
- :response,
- html_url: 'https://github.com/foo/bar/pull/42',
- path: 'README.md',
- commit_id: '123abc',
- original_commit_id: 'original123abc',
- diff_hunk: hunk,
- user: double(:user, id: 4, login: 'alice'),
- body: 'Hello world',
- created_at: created_at,
- updated_at: updated_at,
- line: 23,
- start_line: nil,
- id: 1
- )
- end
-
- it_behaves_like 'a DiffNote' do
- let(:note) { described_class.from_api_response(response) }
- end
-
- it 'does not set the user if the response did not include a user' do
- allow(response)
- .to receive(:user)
- .and_return(nil)
-
- note = described_class.from_api_response(response)
-
- expect(note.author).to be_nil
- end
-
- it 'formats a suggestion in the note body' do
- allow(response)
- .to receive(:body)
- .and_return <<~BODY
- ```suggestion
- Hello World
- ```
- BODY
-
- note = described_class.from_api_response(response)
-
- expect(note.note).to eq <<~BODY
- ```suggestion:-0+0
- Hello World
- ```
- BODY
- end
- end
-
- describe '.from_json_hash' do
- let(:hash) do
- {
- 'noteable_type' => 'MergeRequest',
- 'noteable_id' => 42,
- 'file_path' => 'README.md',
- 'commit_id' => '123abc',
- 'original_commit_id' => 'original123abc',
- 'diff_hunk' => hunk,
- 'author' => { 'id' => 4, 'login' => 'alice' },
- 'note' => 'Hello world',
- 'created_at' => created_at.to_s,
- 'updated_at' => updated_at.to_s,
- 'note_id' => 1
- }
- end
- it_behaves_like 'a DiffNote' do
- let(:note) { described_class.from_json_hash(hash) }
- end
-
- it 'does not convert the author if it was not specified' do
- hash.delete('author')
-
- note = described_class.from_json_hash(hash)
+ describe '#diff_hash' do
+ it 'returns a Hash containing the diff details' do
+ expect(note.diff_hash).to eq(
+ diff: hunk,
+ new_path: 'README.md',
+ old_path: 'README.md',
+ a_mode: '100644',
+ b_mode: '100644',
+ new_file: false
+ )
+ end
+ end
- expect(note.author).to be_nil
- end
+ describe '#diff_position' do
+ before do
+ note.merge_request = double(
+ :merge_request,
+ diff_refs: double(
+ :refs,
+ base_sha: 'base',
+ start_sha: 'start',
+ head_sha: 'head'
+ )
+ )
+ end
+
+ context 'when the diff is an addition' do
+ it 'returns a Gitlab::Diff::Position' do
+ expect(note.diff_position.to_h).to eq(
+ base_sha: 'base',
+ head_sha: 'head',
+ line_range: nil,
+ new_line: 23,
+ new_path: 'README.md',
+ old_line: nil,
+ old_path: 'README.md',
+ position_type: 'text',
+ start_sha: 'start'
+ )
+ end
+ end
+
+ context 'when the diff is an deletion' do
+ let(:side) { 'LEFT' }
+
+ it 'returns a Gitlab::Diff::Position' do
+ expect(note.diff_position.to_h).to eq(
+ base_sha: 'base',
+ head_sha: 'head',
+ line_range: nil,
+ old_line: 23,
+ new_path: 'README.md',
+ new_line: nil,
+ old_path: 'README.md',
+ position_type: 'text',
+ start_sha: 'start'
+ )
+ end
+ end
+ end
- it 'formats a suggestion in the note body' do
- hash['note'] = <<~BODY
- ```suggestion
- Hello World
- ```
- BODY
+ describe '#discussion_id' do
+ before do
+ note.project = project
+ note.merge_request = merge_request
+ end
+
+ context 'when the note is a reply to a discussion' do
+ it 'uses the cached value as the discussion_id only when responding an existing discussion' do
+ expect(Discussion)
+ .to receive(:discussion_id)
+ .and_return('FIRST_DISCUSSION_ID', 'SECOND_DISCUSSION_ID')
+
+ # Creates the first discussion id and caches its value
+ expect(note.discussion_id)
+ .to eq('FIRST_DISCUSSION_ID')
+
+ reply_note = described_class.from_json_hash(
+ 'note_id' => note.note_id + 1,
+ 'in_reply_to_id' => note.note_id
+ )
+ reply_note.project = project
+ reply_note.merge_request = merge_request
+
+ # Reading from the cached value
+ expect(reply_note.discussion_id)
+ .to eq('FIRST_DISCUSSION_ID')
+
+ new_discussion_note = described_class.from_json_hash(
+ 'note_id' => note.note_id + 2,
+ 'in_reply_to_id' => nil
+ )
+ new_discussion_note.project = project
+ new_discussion_note.merge_request = merge_request
+
+ # Because it's a new discussion, it must not use the cached value
+ expect(new_discussion_note.discussion_id)
+ .to eq('SECOND_DISCUSSION_ID')
+ end
+ end
+ end
- note = described_class.from_json_hash(hash)
+ describe '#github_identifiers' do
+ it 'returns a hash with needed identifiers' do
+ expect(note.github_identifiers).to eq(
+ noteable_id: 42,
+ noteable_type: 'MergeRequest',
+ note_id: 1
+ )
+ end
+ end
- expect(note.note).to eq <<~BODY
- ```suggestion:-0+0
- Hello World
- ```
- BODY
- end
- end
+ describe '#line_code' do
+ it 'generates the proper line code' do
+ note = described_class.new(diff_hunk: hunk, file_path: 'README.md')
- describe '#line_code' do
- it 'returns a String' do
- note = described_class.new(diff_hunk: hunk, file_path: 'README.md')
+ expect(note.line_code).to eq('8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_2_2')
+ end
+ end
- expect(note.line_code).to be_an_instance_of(String)
+ describe '#note and #contains_suggestion?' do
+ it 'includes the note body' do
+ expect(note.note).to eq('Hello world')
+ expect(note.contains_suggestion?).to eq(false)
+ end
+
+ context 'when the note have a suggestion' do
+ let(:note_body) do
+ <<~BODY
+ ```suggestion
+ Hello World
+ ```
+ BODY
+ end
+
+ it 'returns the suggestion formatted in the note' do
+ expect(note.note).to eq <<~BODY
+ ```suggestion:-0+0
+ Hello World
+ ```
+ BODY
+ expect(note.contains_suggestion?).to eq(true)
+ end
+ end
+
+ context 'when the note have a multiline suggestion' do
+ let(:start_line) { 20 }
+ let(:end_line) { 23 }
+ let(:note_body) do
+ <<~BODY
+ ```suggestion
+ Hello World
+ ```
+ BODY
+ end
+
+ it 'returns the multi-line suggestion formatted in the note' do
+ expect(note.note).to eq <<~BODY
+ ```suggestion:-3+0
+ Hello World
+ ```
+ BODY
+ expect(note.contains_suggestion?).to eq(true)
+ end
+ end
+
+ describe '#author' do
+ it 'includes the user details' do
+ expect(note.author).to be_an_instance_of(
+ Gitlab::GithubImport::Representation::User
+ )
+
+ expect(note.author.id).to eq(4)
+ expect(note.author.login).to eq('alice')
+ end
+
+ context 'when the author is empty' do
+ let(:user_data) { nil }
+
+ it 'does not set the user if the response did not include a user' do
+ expect(note.author).to be_nil
+ end
+ end
+ end
+ end
end
end
- describe '#diff_hash' do
- it 'returns a Hash containing the diff details' do
- note = described_class.from_json_hash(
- 'noteable_type' => 'MergeRequest',
- 'noteable_id' => 42,
- 'file_path' => 'README.md',
- 'commit_id' => '123abc',
- 'original_commit_id' => 'original123abc',
- 'diff_hunk' => hunk,
- 'author' => { 'id' => 4, 'login' => 'alice' },
- 'note' => 'Hello world',
- 'created_at' => created_at.to_s,
- 'updated_at' => updated_at.to_s,
- 'note_id' => 1
- )
-
- expect(note.diff_hash).to eq(
- diff: hunk,
- new_path: 'README.md',
- old_path: 'README.md',
- a_mode: '100644',
- b_mode: '100644',
- new_file: false
- )
- end
- end
+ describe '.from_api_response' do
+ it_behaves_like 'a DiffNote representation' do
+ let(:response) do
+ double(
+ :response,
+ id: note_id,
+ html_url: 'https://github.com/foo/bar/pull/42',
+ path: 'README.md',
+ commit_id: '123abc',
+ original_commit_id: 'original123abc',
+ side: side,
+ user: user_data && double(:user, user_data),
+ diff_hunk: hunk,
+ body: note_body,
+ created_at: created_at,
+ updated_at: updated_at,
+ line: end_line,
+ start_line: start_line,
+ in_reply_to_id: in_reply_to_id
+ )
+ end
- describe '#github_identifiers' do
- it 'returns a hash with needed identifiers' do
- github_identifiers = {
- noteable_id: 42,
- noteable_type: 'MergeRequest',
- note_id: 1
- }
- other_attributes = { something_else: '_something_else_' }
- note = described_class.new(github_identifiers.merge(other_attributes))
-
- expect(note.github_identifiers).to eq(github_identifiers)
+ subject(:note) { described_class.from_api_response(response) }
end
end
- describe '#note' do
- it 'returns the given note' do
- hash = {
- 'note': 'simple text'
- }
-
- note = described_class.new(hash)
-
- expect(note.note).to eq 'simple text'
- end
-
- it 'returns the suggestion formatted in the note' do
- hash = {
- 'note': <<~BODY
- ```suggestion
- Hello World
- ```
- BODY
- }
-
- note = described_class.new(hash)
-
- expect(note.note).to eq <<~BODY
- ```suggestion:-0+0
- Hello World
- ```
- BODY
- end
+ describe '.from_json_hash' do
+ it_behaves_like 'a DiffNote representation' do
+ let(:hash) do
+ {
+ 'note_id' => note_id,
+ 'noteable_type' => 'MergeRequest',
+ 'noteable_id' => 42,
+ 'file_path' => 'README.md',
+ 'commit_id' => '123abc',
+ 'original_commit_id' => 'original123abc',
+ 'side' => side,
+ 'author' => user_data,
+ 'diff_hunk' => hunk,
+ 'note' => note_body,
+ 'created_at' => created_at.to_s,
+ 'updated_at' => updated_at.to_s,
+ 'end_line' => end_line,
+ 'start_line' => start_line,
+ 'in_reply_to_id' => in_reply_to_id
+ }
+ end
- it 'returns the multi-line suggestion formatted in the note' do
- hash = {
- 'start_line': 20,
- 'end_line': 23,
- 'note': <<~BODY
- ```suggestion
- Hello World
- ```
- BODY
- }
-
- note = described_class.new(hash)
-
- expect(note.note).to eq <<~BODY
- ```suggestion:-3+0
- Hello World
- ```
- BODY
+ subject(:note) { described_class.from_json_hash(hash) }
end
end
end
diff --git a/spec/lib/gitlab/github_import/representation/diff_notes/suggestion_formatter_spec.rb b/spec/lib/gitlab/github_import/representation/diff_notes/suggestion_formatter_spec.rb
index 2ffd5f50d3b..bcb8575bdbf 100644
--- a/spec/lib/gitlab/github_import/representation/diff_notes/suggestion_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/representation/diff_notes/suggestion_formatter_spec.rb
@@ -9,13 +9,19 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNotes::SuggestionFormat
```
BODY
- expect(described_class.formatted_note_for(note: note)).to eq(note)
+ note_formatter = described_class.new(note: note)
+
+ expect(note_formatter.formatted_note).to eq(note)
+ expect(note_formatter.contains_suggestion?).to eq(false)
end
it 'handles nil value for note' do
note = nil
- expect(described_class.formatted_note_for(note: note)).to eq(note)
+ note_formatter = described_class.new(note: note)
+
+ expect(note_formatter.formatted_note).to eq(note)
+ expect(note_formatter.contains_suggestion?).to eq(false)
end
it 'does not allow over 3 leading spaces for valid suggestion' do
@@ -26,7 +32,10 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNotes::SuggestionFormat
```
BODY
- expect(described_class.formatted_note_for(note: note)).to eq(note)
+ note_formatter = described_class.new(note: note)
+
+ expect(note_formatter.formatted_note).to eq(note)
+ expect(note_formatter.contains_suggestion?).to eq(false)
end
it 'allows up to 3 leading spaces' do
@@ -44,7 +53,10 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNotes::SuggestionFormat
```
BODY
- expect(described_class.formatted_note_for(note: note)).to eq(expected)
+ note_formatter = described_class.new(note: note)
+
+ expect(note_formatter.formatted_note).to eq(expected)
+ expect(note_formatter.contains_suggestion?).to eq(true)
end
it 'does nothing when there is any text without space after the suggestion tag' do
@@ -53,7 +65,10 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNotes::SuggestionFormat
```
BODY
- expect(described_class.formatted_note_for(note: note)).to eq(note)
+ note_formatter = described_class.new(note: note)
+
+ expect(note_formatter.formatted_note).to eq(note)
+ expect(note_formatter.contains_suggestion?).to eq(false)
end
it 'formats single-line suggestions' do
@@ -71,7 +86,10 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNotes::SuggestionFormat
```
BODY
- expect(described_class.formatted_note_for(note: note)).to eq(expected)
+ note_formatter = described_class.new(note: note)
+
+ expect(note_formatter.formatted_note).to eq(expected)
+ expect(note_formatter.contains_suggestion?).to eq(true)
end
it 'ignores text after suggestion tag on the same line' do
@@ -89,7 +107,10 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNotes::SuggestionFormat
```
BODY
- expect(described_class.formatted_note_for(note: note)).to eq(expected)
+ note_formatter = described_class.new(note: note)
+
+ expect(note_formatter.formatted_note).to eq(expected)
+ expect(note_formatter.contains_suggestion?).to eq(true)
end
it 'formats multiple single-line suggestions' do
@@ -115,7 +136,10 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNotes::SuggestionFormat
```
BODY
- expect(described_class.formatted_note_for(note: note)).to eq(expected)
+ note_formatter = described_class.new(note: note)
+
+ expect(note_formatter.formatted_note).to eq(expected)
+ expect(note_formatter.contains_suggestion?).to eq(true)
end
it 'formats multi-line suggestions' do
@@ -133,7 +157,10 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNotes::SuggestionFormat
```
BODY
- expect(described_class.formatted_note_for(note: note, start_line: 6, end_line: 8)).to eq(expected)
+ note_formatter = described_class.new(note: note, start_line: 6, end_line: 8)
+
+ expect(note_formatter.formatted_note).to eq(expected)
+ expect(note_formatter.contains_suggestion?).to eq(true)
end
it 'formats multiple multi-line suggestions' do
@@ -159,6 +186,9 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNotes::SuggestionFormat
```
BODY
- expect(described_class.formatted_note_for(note: note, start_line: 6, end_line: 8)).to eq(expected)
+ note_formatter = described_class.new(note: note, start_line: 6, end_line: 8)
+
+ expect(note_formatter.formatted_note).to eq(expected)
+ expect(note_formatter.contains_suggestion?).to eq(true)
end
end
diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb
index 55102554508..20d5972bd88 100644
--- a/spec/lib/gitlab/gpg/commit_spec.rb
+++ b/spec/lib/gitlab/gpg/commit_spec.rb
@@ -136,7 +136,7 @@ RSpec.describe Gitlab::Gpg::Commit do
it 'returns a valid signature' do
verified_signature = double('verified-signature', fingerprint: GpgHelpers::User1.fingerprint, valid?: true)
allow(GPGME::Crypto).to receive(:new).and_return(crypto)
- allow(crypto).to receive(:verify).and_return(verified_signature)
+ allow(crypto).to receive(:verify).and_yield(verified_signature)
signature = described_class.new(commit).signature
@@ -178,7 +178,7 @@ RSpec.describe Gitlab::Gpg::Commit do
keyid = GpgHelpers::User1.fingerprint.last(16)
verified_signature = double('verified-signature', fingerprint: keyid, valid?: true)
allow(GPGME::Crypto).to receive(:new).and_return(crypto)
- allow(crypto).to receive(:verify).and_return(verified_signature)
+ allow(crypto).to receive(:verify).and_yield(verified_signature)
signature = described_class.new(commit).signature
@@ -194,6 +194,71 @@ RSpec.describe Gitlab::Gpg::Commit do
end
end
+ context 'commit with multiple signatures' do
+ let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User1.emails.first }
+
+ let!(:user) { create(:user, email: GpgHelpers::User1.emails.first) }
+
+ let!(:gpg_key) do
+ create :gpg_key, key: GpgHelpers::User1.public_key, user: user
+ end
+
+ let!(:crypto) { instance_double(GPGME::Crypto) }
+
+ before do
+ fake_signature = [
+ GpgHelpers::User1.signed_commit_signature,
+ GpgHelpers::User1.signed_commit_base_data
+ ]
+
+ allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily)
+ .with(Gitlab::Git::Repository, commit_sha)
+ .and_return(fake_signature)
+ end
+
+ it 'returns an invalid signatures error' do
+ verified_signature = double('verified-signature', fingerprint: GpgHelpers::User1.fingerprint, valid?: true)
+ allow(GPGME::Crypto).to receive(:new).and_return(crypto)
+ allow(crypto).to receive(:verify).and_yield(verified_signature).and_yield(verified_signature)
+
+ signature = described_class.new(commit).signature
+
+ expect(signature).to have_attributes(
+ commit_sha: commit_sha,
+ project: project,
+ gpg_key: gpg_key,
+ gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
+ gpg_key_user_name: GpgHelpers::User1.names.first,
+ gpg_key_user_email: GpgHelpers::User1.emails.first,
+ verification_status: 'multiple_signatures'
+ )
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(multiple_gpg_signatures: false)
+ end
+
+ it 'returns an valid signature' do
+ verified_signature = double('verified-signature', fingerprint: GpgHelpers::User1.fingerprint, valid?: true)
+ allow(GPGME::Crypto).to receive(:new).and_return(crypto)
+ allow(crypto).to receive(:verify).and_yield(verified_signature).and_yield(verified_signature)
+
+ signature = described_class.new(commit).signature
+
+ expect(signature).to have_attributes(
+ commit_sha: commit_sha,
+ project: project,
+ gpg_key: gpg_key,
+ gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
+ gpg_key_user_name: GpgHelpers::User1.names.first,
+ gpg_key_user_email: GpgHelpers::User1.emails.first,
+ verification_status: 'verified'
+ )
+ end
+ end
+ end
+
context 'commit signed with a subkey' do
let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User3.emails.first }
diff --git a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb
index c1516a48b80..771f6e1ec46 100644
--- a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb
+++ b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb
@@ -140,6 +140,8 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
key: GpgHelpers::User1.public_key,
user: user
+ user.reload # necessary to reload the association with gpg_keys
+
expect(invalid_gpg_signature.reload.verification_status).to eq 'unverified_key'
# InvalidGpgSignatureUpdater is called by the after_update hook
diff --git a/spec/lib/gitlab/grape_logging/loggers/perf_logger_spec.rb b/spec/lib/gitlab/grape_logging/loggers/perf_logger_spec.rb
index 641fb27a071..ef4bc0ca104 100644
--- a/spec/lib/gitlab/grape_logging/loggers/perf_logger_spec.rb
+++ b/spec/lib/gitlab/grape_logging/loggers/perf_logger_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::GrapeLogging::Loggers::PerfLogger do
- let(:mock_request) { OpenStruct.new(env: {}) }
+ let(:mock_request) { double('env', env: {}) }
describe ".parameters" do
subject { described_class.new.parameters(mock_request, nil) }
diff --git a/spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb b/spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb
index 9538c4bae2b..4cd9f9dfad0 100644
--- a/spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb
+++ b/spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Gitlab::GrapeLogging::Loggers::QueueDurationLogger do
let(:start_time) { Time.new(2018, 01, 01) }
describe 'when no proxy time is available' do
- let(:mock_request) { OpenStruct.new(env: {}) }
+ let(:mock_request) { double('env', env: {}) }
it 'returns an empty hash' do
expect(subject.parameters(mock_request, nil)).to eq({})
@@ -18,7 +18,7 @@ RSpec.describe Gitlab::GrapeLogging::Loggers::QueueDurationLogger do
describe 'when a proxy time is available' do
let(:mock_request) do
- OpenStruct.new(
+ double('env',
env: {
'HTTP_GITLAB_WORKHORSE_PROXY_START' => (start_time - 1.hour).to_i * (10**9)
}
diff --git a/spec/lib/gitlab/grape_logging/loggers/urgency_logger_spec.rb b/spec/lib/gitlab/grape_logging/loggers/urgency_logger_spec.rb
new file mode 100644
index 00000000000..464534f0271
--- /dev/null
+++ b/spec/lib/gitlab/grape_logging/loggers/urgency_logger_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GrapeLogging::Loggers::UrgencyLogger do
+ def endpoint(options, namespace: '')
+ Struct.new(:options, :namespace).new(options, namespace)
+ end
+
+ let(:api_class) do
+ Class.new(API::Base) do
+ namespace 'testing' do
+ # rubocop:disable Rails/HttpPositionalArguments
+ # This is not the get that performs a request, but the one from Grape
+ get 'test', urgency: :high do
+ {}
+ end
+ # rubocop:enable Rails/HttpPositionalArguments
+ end
+ end
+ end
+
+ describe ".parameters" do
+ where(:request_env, :expected_parameters) do
+ [
+ [{}, {}],
+ [{ 'api.endpoint' => endpoint({}) }, {}],
+ [{ 'api.endpoint' => endpoint({ for: 'something weird' }) }, {}],
+ [
+ { 'api.endpoint' => endpoint({ for: api_class, path: [] }) },
+ { request_urgency: :default, target_duration_s: 1 }
+ ],
+ [
+ { 'api.endpoint' => endpoint({ for: api_class, path: ['test'] }, namespace: '/testing') },
+ { request_urgency: :high, target_duration_s: 0.25 }
+ ]
+ ]
+ end
+
+ with_them do
+ let(:request) { double('request', env: request_env) }
+
+ subject { described_class.new.parameters(request, nil) }
+
+ it { is_expected.to eq(expected_parameters) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/graphql/known_operations_spec.rb b/spec/lib/gitlab/graphql/known_operations_spec.rb
new file mode 100644
index 00000000000..411c0876f82
--- /dev/null
+++ b/spec/lib/gitlab/graphql/known_operations_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'rspec-parameterized'
+require "support/graphql/fake_query_type"
+
+RSpec.describe Gitlab::Graphql::KnownOperations do
+ using RSpec::Parameterized::TableSyntax
+
+ # Include duplicated operation names to test that we are unique-ifying them
+ let(:fake_operations) { %w(foo foo bar bar) }
+ let(:fake_schema) do
+ Class.new(GraphQL::Schema) do
+ query Graphql::FakeQueryType
+ end
+ end
+
+ subject { described_class.new(fake_operations) }
+
+ describe "#from_query" do
+ where(:query_string, :expected) do
+ "query { helloWorld }" | described_class::ANONYMOUS
+ "query fuzzyyy { helloWorld }" | described_class::UNKNOWN
+ "query foo { helloWorld }" | described_class::Operation.new("foo")
+ end
+
+ with_them do
+ it "returns known operation name from GraphQL Query" do
+ query = ::GraphQL::Query.new(fake_schema, query_string)
+
+ expect(subject.from_query(query)).to eq(expected)
+ end
+ end
+ end
+
+ describe "#operations" do
+ it "returns array of known operations" do
+ expect(subject.operations.map(&:name)).to match_array(%w(anonymous unknown foo bar))
+ end
+ end
+
+ describe "Operation#to_caller_id" do
+ where(:query_string, :expected) do
+ "query { helloWorld }" | "graphql:#{described_class::ANONYMOUS.name}"
+ "query foo { helloWorld }" | "graphql:foo"
+ end
+
+ with_them do
+ it "formats operation name for caller_id metric property" do
+ query = ::GraphQL::Query.new(fake_schema, query_string)
+
+ expect(subject.from_query(query).to_caller_id).to eq(expected)
+ end
+ end
+ end
+
+ describe "Opeartion#query_urgency" do
+ it "returns the associated query urgency" do
+ query = ::GraphQL::Query.new(fake_schema, "query foo { helloWorld }")
+
+ expect(subject.from_query(query).query_urgency).to equal(::Gitlab::EndpointAttributes::DEFAULT_URGENCY)
+ end
+ end
+
+ describe ".default" do
+ it "returns a memoization of values from webpack", :aggregate_failures do
+ # .default could have been referenced in another spec, so we need to clean it up here
+ described_class.instance_variable_set(:@default, nil)
+
+ expect(Gitlab::Webpack::GraphqlKnownOperations).to receive(:load).once.and_return(fake_operations)
+
+ 2.times { described_class.default }
+
+ # Uses reference equality to verify memoization
+ expect(described_class.default).to equal(described_class.default)
+ expect(described_class.default).to be_a(described_class)
+ expect(described_class.default.operations.map(&:name)).to include(*fake_operations)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/graphql/pagination/connections_spec.rb b/spec/lib/gitlab/graphql/pagination/connections_spec.rb
index f3f59113c81..97389b6250e 100644
--- a/spec/lib/gitlab/graphql/pagination/connections_spec.rb
+++ b/spec/lib/gitlab/graphql/pagination/connections_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe ::Gitlab::Graphql::Pagination::Connections do
before(:all) do
ActiveRecord::Schema.define do
- create_table :testing_pagination_nodes, force: true do |t|
+ create_table :_test_testing_pagination_nodes, force: true do |t|
t.integer :value, null: false
end
end
@@ -16,13 +16,13 @@ RSpec.describe ::Gitlab::Graphql::Pagination::Connections do
after(:all) do
ActiveRecord::Schema.define do
- drop_table :testing_pagination_nodes, force: true
+ drop_table :_test_testing_pagination_nodes, force: true
end
end
let_it_be(:node_model) do
Class.new(ActiveRecord::Base) do
- self.table_name = 'testing_pagination_nodes'
+ self.table_name = '_test_testing_pagination_nodes'
end
end
diff --git a/spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb b/spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb
index fc723138d88..dee8f9e3c64 100644
--- a/spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb
+++ b/spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb
@@ -18,12 +18,6 @@ RSpec.describe Gitlab::Graphql::QueryAnalyzers::LoggerAnalyzer do
GRAPHQL
end
- describe 'variables' do
- subject { initial_value.fetch(:variables) }
-
- it { is_expected.to eq('{:body=>"[FILTERED]"}') }
- end
-
describe '#final_value' do
let(:monotonic_time_before) { 42 }
let(:monotonic_time_after) { 500 }
@@ -42,7 +36,14 @@ RSpec.describe Gitlab::Graphql::QueryAnalyzers::LoggerAnalyzer do
it 'inserts duration in seconds to memo and sets request store' do
expect { final_value }.to change { memo[:duration_s] }.to(monotonic_time_duration)
- .and change { RequestStore.store[:graphql_logs] }.to([memo])
+ .and change { RequestStore.store[:graphql_logs] }.to([{
+ complexity: 4,
+ depth: 2,
+ operation_name: query.operation_name,
+ used_deprecated_fields: [],
+ used_fields: [],
+ variables: { body: "[FILTERED]" }.to_s
+ }])
end
end
end
diff --git a/spec/lib/gitlab/graphql/tracers/application_context_tracer_spec.rb b/spec/lib/gitlab/graphql/tracers/application_context_tracer_spec.rb
new file mode 100644
index 00000000000..6eff816b95a
--- /dev/null
+++ b/spec/lib/gitlab/graphql/tracers/application_context_tracer_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+require "fast_spec_helper"
+require "support/graphql/fake_tracer"
+require "support/graphql/fake_query_type"
+
+RSpec.describe Gitlab::Graphql::Tracers::ApplicationContextTracer do
+ let(:tracer_spy) { spy('tracer_spy') }
+ let(:default_known_operations) { ::Gitlab::Graphql::KnownOperations.new(['fooOperation']) }
+ let(:dummy_schema) do
+ schema = Class.new(GraphQL::Schema) do
+ use Gitlab::Graphql::Tracers::ApplicationContextTracer
+
+ query Graphql::FakeQueryType
+ end
+
+ fake_tracer = Graphql::FakeTracer.new(lambda do |key, *args|
+ tracer_spy.trace(key, Gitlab::ApplicationContext.current)
+ end)
+
+ schema.tracer(fake_tracer)
+
+ schema
+ end
+
+ before do
+ allow(::Gitlab::Graphql::KnownOperations).to receive(:default).and_return(default_known_operations)
+ end
+
+ it "sets application context during execute_query and cleans up afterwards", :aggregate_failures do
+ dummy_schema.execute("query fooOperation { helloWorld }")
+
+ # "parse" is just an arbitrary trace event that isn't setting caller_id
+ expect(tracer_spy).to have_received(:trace).with("parse", hash_excluding("meta.caller_id"))
+ expect(tracer_spy).to have_received(:trace).with("execute_query", hash_including("meta.caller_id" => "graphql:fooOperation")).once
+ expect(Gitlab::ApplicationContext.current).not_to include("meta.caller_id")
+ end
+
+ it "sets caller_id when operation is not known" do
+ dummy_schema.execute("query fuzz { helloWorld }")
+
+ expect(tracer_spy).to have_received(:trace).with("execute_query", hash_including("meta.caller_id" => "graphql:unknown")).once
+ end
+end
diff --git a/spec/lib/gitlab/graphql/tracers/logger_tracer_spec.rb b/spec/lib/gitlab/graphql/tracers/logger_tracer_spec.rb
new file mode 100644
index 00000000000..d83ac4dabc5
--- /dev/null
+++ b/spec/lib/gitlab/graphql/tracers/logger_tracer_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+require "fast_spec_helper"
+require "support/graphql/fake_query_type"
+
+RSpec.describe Gitlab::Graphql::Tracers::LoggerTracer do
+ let(:dummy_schema) do
+ Class.new(GraphQL::Schema) do
+ # LoggerTracer depends on TimerTracer
+ use Gitlab::Graphql::Tracers::LoggerTracer
+ use Gitlab::Graphql::Tracers::TimerTracer
+
+ query_analyzer Gitlab::Graphql::QueryAnalyzers::LoggerAnalyzer.new
+
+ query Graphql::FakeQueryType
+ end
+ end
+
+ around do |example|
+ Gitlab::ApplicationContext.with_context(caller_id: 'caller_a', feature_category: 'feature_a') do
+ example.run
+ end
+ end
+
+ it "logs every query", :aggregate_failures do
+ variables = { name: "Ada Lovelace" }
+ query_string = 'query fooOperation($name: String) { helloWorld(message: $name) }'
+
+ # Build an actual query so we don't have to hardocde the "fingerprint" calculations
+ query = GraphQL::Query.new(dummy_schema, query_string, variables: variables)
+
+ expect(::Gitlab::GraphqlLogger).to receive(:info).with({
+ "correlation_id" => anything,
+ "meta.caller_id" => "caller_a",
+ "meta.feature_category" => "feature_a",
+ "query_analysis.duration_s" => kind_of(Numeric),
+ "query_analysis.complexity" => 1,
+ "query_analysis.depth" => 1,
+ "query_analysis.used_deprecated_fields" => [],
+ "query_analysis.used_fields" => ["FakeQuery.helloWorld"],
+ duration_s: be > 0,
+ is_mutation: false,
+ operation_fingerprint: query.operation_fingerprint,
+ operation_name: 'fooOperation',
+ query_fingerprint: query.fingerprint,
+ query_string: query_string,
+ trace_type: "execute_query",
+ variables: variables.to_s
+ })
+
+ dummy_schema.execute(query_string, variables: variables)
+ end
+end
diff --git a/spec/lib/gitlab/graphql/tracers/metrics_tracer_spec.rb b/spec/lib/gitlab/graphql/tracers/metrics_tracer_spec.rb
new file mode 100644
index 00000000000..ff6a76aa319
--- /dev/null
+++ b/spec/lib/gitlab/graphql/tracers/metrics_tracer_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'rspec-parameterized'
+require "support/graphql/fake_query_type"
+
+RSpec.describe Gitlab::Graphql::Tracers::MetricsTracer do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:default_known_operations) { ::Gitlab::Graphql::KnownOperations.new(%w(lorem foo bar)) }
+
+ let(:fake_schema) do
+ Class.new(GraphQL::Schema) do
+ use Gitlab::Graphql::Tracers::ApplicationContextTracer
+ use Gitlab::Graphql::Tracers::MetricsTracer
+ use Gitlab::Graphql::Tracers::TimerTracer
+
+ query Graphql::FakeQueryType
+ end
+ end
+
+ around do |example|
+ ::Gitlab::ApplicationContext.with_context(feature_category: 'test_feature_category') do
+ example.run
+ end
+ end
+
+ before do
+ allow(::Gitlab::Graphql::KnownOperations).to receive(:default).and_return(default_known_operations)
+ end
+
+ describe 'when used as tracer and query is executed' do
+ where(:duration, :expected_success) do
+ 0.1 | true
+ 0.1 + ::Gitlab::EndpointAttributes::DEFAULT_URGENCY.duration | false
+ end
+
+ with_them do
+ it 'increments sli' do
+ # Trigger initialization
+ fake_schema
+
+ # setup timer
+ current_time = 0
+ allow(Gitlab::Metrics::System).to receive(:monotonic_time) { current_time += duration }
+
+ expect(Gitlab::Metrics::RailsSlis.graphql_query_apdex).to receive(:increment).with(
+ labels: {
+ endpoint_id: 'graphql:lorem',
+ feature_category: 'test_feature_category',
+ query_urgency: ::Gitlab::EndpointAttributes::DEFAULT_URGENCY.name
+ },
+ success: expected_success
+ )
+
+ fake_schema.execute("query lorem { helloWorld }")
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/graphql/tracers/timer_tracer_spec.rb b/spec/lib/gitlab/graphql/tracers/timer_tracer_spec.rb
new file mode 100644
index 00000000000..7f837e28772
--- /dev/null
+++ b/spec/lib/gitlab/graphql/tracers/timer_tracer_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+require "fast_spec_helper"
+require "support/graphql/fake_tracer"
+require "support/graphql/fake_query_type"
+
+RSpec.describe Gitlab::Graphql::Tracers::TimerTracer do
+ let(:expected_duration) { 5 }
+ let(:tracer_spy) { spy('tracer_spy') }
+ let(:dummy_schema) do
+ schema = Class.new(GraphQL::Schema) do
+ use Gitlab::Graphql::Tracers::TimerTracer
+
+ query Graphql::FakeQueryType
+ end
+
+ schema.tracer(Graphql::FakeTracer.new(lambda { |*args| tracer_spy.trace(*args) }))
+
+ schema
+ end
+
+ before do
+ current_time = 0
+ allow(Gitlab::Metrics::System).to receive(:monotonic_time) do
+ current_time += expected_duration
+ end
+ end
+
+ it "adds duration_s to the trace metadata", :aggregate_failures do
+ query_string = "query fooOperation { helloWorld }"
+
+ dummy_schema.execute(query_string)
+
+ # "parse" and "execute_query" are just arbitrary trace events
+ expect(tracer_spy).to have_received(:trace).with("parse", {
+ duration_s: expected_duration,
+ query_string: query_string
+ })
+ expect(tracer_spy).to have_received(:trace).with("execute_query", {
+ # greater than expected duration because other calls made to `.monotonic_time` are outside our control
+ duration_s: be >= expected_duration,
+ query: instance_of(GraphQL::Query)
+ })
+ end
+end
diff --git a/spec/lib/gitlab/health_checks/redis/redis_check_spec.rb b/spec/lib/gitlab/health_checks/redis/redis_check_spec.rb
index 43e890a6c4f..145d573b6de 100644
--- a/spec/lib/gitlab/health_checks/redis/redis_check_spec.rb
+++ b/spec/lib/gitlab/health_checks/redis/redis_check_spec.rb
@@ -4,5 +4,5 @@ require 'spec_helper'
require_relative '../simple_check_shared'
RSpec.describe Gitlab::HealthChecks::Redis::RedisCheck do
- include_examples 'simple_check', 'redis_ping', 'Redis', 'PONG'
+ include_examples 'simple_check', 'redis_ping', 'Redis', true
end
diff --git a/spec/lib/gitlab/import/database_helpers_spec.rb b/spec/lib/gitlab/import/database_helpers_spec.rb
index 079faed2518..05d1c0ae078 100644
--- a/spec/lib/gitlab/import/database_helpers_spec.rb
+++ b/spec/lib/gitlab/import/database_helpers_spec.rb
@@ -16,8 +16,8 @@ RSpec.describe Gitlab::Import::DatabaseHelpers do
let(:project) { create(:project) }
it 'returns the ID returned by the query' do
- expect(Gitlab::Database.main)
- .to receive(:bulk_insert)
+ expect(ApplicationRecord)
+ .to receive(:legacy_bulk_insert)
.with(Issue.table_name, [attributes], return_ids: true)
.and_return([10])
diff --git a/spec/lib/gitlab/import/metrics_spec.rb b/spec/lib/gitlab/import/metrics_spec.rb
index 035294a620f..9b8b58d00f3 100644
--- a/spec/lib/gitlab/import/metrics_spec.rb
+++ b/spec/lib/gitlab/import/metrics_spec.rb
@@ -94,20 +94,6 @@ RSpec.describe Gitlab::Import::Metrics, :aggregate_failures do
expect(histogram).to have_received(:observe).with({ importer: :test_importer }, anything)
end
end
-
- context 'when project is a github import' do
- before do
- project.import_type = 'github'
- end
-
- it 'emits importer metrics' do
- expect(subject).to receive(:track_usage_event).with(:github_import_project_success, project.id)
-
- subject.track_finished_import
-
- expect(histogram).to have_received(:observe).with({ project: project.full_path }, anything)
- end
- end
end
describe '#issues_counter' do
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 10f0e687077..b474f5825fd 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -60,6 +60,7 @@ issues:
- incident_management_issuable_escalation_status
- pending_escalations
- customer_relations_contacts
+- issue_customer_relations_contacts
work_item_type:
- issues
events:
@@ -132,6 +133,7 @@ project_members:
- user
- source
- project
+- member_task
merge_requests:
- status_check_responses
- subscriptions
@@ -382,6 +384,7 @@ project:
- emails_on_push_integration
- pipelines_email_integration
- mattermost_slash_commands_integration
+- shimo_integration
- slack_slash_commands_integration
- irker_integration
- packagist_integration
diff --git a/spec/lib/gitlab/import_export/attributes_permitter_spec.rb b/spec/lib/gitlab/import_export/attributes_permitter_spec.rb
index 2b974f8985d..8ae387d95e3 100644
--- a/spec/lib/gitlab/import_export/attributes_permitter_spec.rb
+++ b/spec/lib/gitlab/import_export/attributes_permitter_spec.rb
@@ -80,25 +80,66 @@ RSpec.describe Gitlab::ImportExport::AttributesPermitter do
let(:attributes_permitter) { described_class.new }
- where(:relation_name, :permitted_attributes_defined) do
- :user | false
- :author | false
- :ci_cd_settings | true
- :metrics_setting | true
- :project_badges | true
- :pipeline_schedules | true
- :error_tracking_setting | true
- :auto_devops | true
- :boards | true
- :custom_attributes | true
- :labels | true
- :protected_branches | true
- :protected_tags | true
- :create_access_levels | true
- :merge_access_levels | true
- :push_access_levels | true
- :releases | true
- :links | true
+ where(:relation_name, :permitted_attributes_defined ) do
+ :user | true
+ :author | false
+ :ci_cd_settings | true
+ :metrics_setting | true
+ :project_badges | true
+ :pipeline_schedules | true
+ :error_tracking_setting | true
+ :auto_devops | true
+ :boards | true
+ :custom_attributes | true
+ :label | true
+ :labels | true
+ :protected_branches | true
+ :protected_tags | true
+ :create_access_levels | true
+ :merge_access_levels | true
+ :push_access_levels | true
+ :releases | true
+ :links | true
+ :priorities | true
+ :milestone | true
+ :milestones | true
+ :snippets | true
+ :project_members | true
+ :merge_request | true
+ :merge_requests | true
+ :award_emoji | true
+ :commit_author | true
+ :committer | true
+ :events | true
+ :label_links | true
+ :merge_request_diff | true
+ :merge_request_diff_commits | true
+ :merge_request_diff_files | true
+ :metrics | true
+ :notes | true
+ :push_event_payload | true
+ :resource_label_events | true
+ :suggestions | true
+ :system_note_metadata | true
+ :timelogs | true
+ :container_expiration_policy | true
+ :project_feature | true
+ :prometheus_metrics | true
+ :service_desk_setting | true
+ :external_pull_request | true
+ :external_pull_requests | true
+ :statuses | true
+ :ci_pipelines | true
+ :stages | true
+ :actions | true
+ :design | true
+ :designs | true
+ :design_versions | true
+ :issue_assignees | true
+ :sentry_issue | true
+ :zoom_meetings | true
+ :issues | true
+ :group_members | true
end
with_them do
@@ -109,9 +150,11 @@ RSpec.describe Gitlab::ImportExport::AttributesPermitter do
describe 'included_attributes for Project' do
subject { described_class.new }
+ additional_attributes = { user: %w[id] }
+
Gitlab::ImportExport::Config.new.to_h[:included_attributes].each do |relation_sym, permitted_attributes|
context "for #{relation_sym}" do
- it_behaves_like 'a permitted attribute', relation_sym, permitted_attributes
+ it_behaves_like 'a permitted attribute', relation_sym, permitted_attributes, additional_attributes[relation_sym]
end
end
end
diff --git a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
index fc08a13a8bd..d5f31f235f5 100644
--- a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
+++ b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
@@ -207,9 +207,9 @@ RSpec.describe Gitlab::ImportExport::FastHashSerializer do
context 'relation ordering' do
it 'orders exported pipelines by primary key' do
- expected_order = project.ci_pipelines.reorder(:id).ids
+ expected_order = project.ci_pipelines.reorder(:id).pluck(:sha)
- expect(subject['ci_pipelines'].pluck('id')).to eq(expected_order)
+ expect(subject['ci_pipelines'].pluck('sha')).to eq(expected_order)
end
end
diff --git a/spec/lib/gitlab/import_export/group/relation_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/group/relation_tree_restorer_spec.rb
new file mode 100644
index 00000000000..473dbf5ecc5
--- /dev/null
+++ b/spec/lib/gitlab/import_export/group/relation_tree_restorer_spec.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+# This spec is a lightweight version of:
+# * project/tree_restorer_spec.rb
+#
+# In depth testing is being done in the above specs.
+# This spec tests that restore project works
+# but does not have 100% relation coverage.
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::ImportExport::Group::RelationTreeRestorer do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:importable) { create(:group, parent: group) }
+
+ include_context 'relation tree restorer shared context' do
+ let(:importable_name) { nil }
+ end
+
+ let(:path) { 'spec/fixtures/lib/gitlab/import_export/group_exports/no_children/group.json' }
+ let(:relation_reader) do
+ Gitlab::ImportExport::Json::LegacyReader::File.new(
+ path,
+ relation_names: reader.group_relation_names)
+ end
+
+ let(:reader) do
+ Gitlab::ImportExport::Reader.new(
+ shared: shared,
+ config: Gitlab::ImportExport::Config.new(config: Gitlab::ImportExport.legacy_group_config_file).to_h
+ )
+ end
+
+ let(:relation_tree_restorer) do
+ described_class.new(
+ user: user,
+ shared: shared,
+ relation_reader: relation_reader,
+ object_builder: Gitlab::ImportExport::Group::ObjectBuilder,
+ members_mapper: members_mapper,
+ relation_factory: Gitlab::ImportExport::Group::RelationFactory,
+ reader: reader,
+ importable: importable,
+ importable_path: nil,
+ importable_attributes: attributes
+ )
+ end
+
+ subject { relation_tree_restorer.restore }
+
+ shared_examples 'logging of relations creation' do
+ context 'when log_import_export_relation_creation feature flag is enabled' do
+ before do
+ stub_feature_flags(log_import_export_relation_creation: group)
+ end
+
+ it 'logs top-level relation creation' do
+ expect(shared.logger)
+ .to receive(:info)
+ .with(hash_including(message: '[Project/Group Import] Created new object relation'))
+ .at_least(:once)
+
+ subject
+ end
+ end
+
+ context 'when log_import_export_relation_creation feature flag is disabled' do
+ before do
+ stub_feature_flags(log_import_export_relation_creation: false)
+ end
+
+ it 'does not log top-level relation creation' do
+ expect(shared.logger)
+ .to receive(:info)
+ .with(hash_including(message: '[Project/Group Import] Created new object relation'))
+ .never
+
+ subject
+ end
+ end
+ end
+
+ it 'restores group tree' do
+ expect(subject).to eq(true)
+ end
+
+ include_examples 'logging of relations creation'
+end
diff --git a/spec/lib/gitlab/import_export/project/object_builder_spec.rb b/spec/lib/gitlab/import_export/project/object_builder_spec.rb
index 4c9f9f7c690..189b798c2e8 100644
--- a/spec/lib/gitlab/import_export/project/object_builder_spec.rb
+++ b/spec/lib/gitlab/import_export/project/object_builder_spec.rb
@@ -123,6 +123,24 @@ RSpec.describe Gitlab::ImportExport::Project::ObjectBuilder do
expect(milestone.persisted?).to be true
end
+
+ context 'with clashing iid' do
+ it 'creates milestone and claims iid for the new milestone' do
+ clashing_iid = 1
+ create(:milestone, iid: clashing_iid, project: project)
+
+ milestone = described_class.build(Milestone,
+ 'iid' => clashing_iid,
+ 'title' => 'milestone',
+ 'project' => project,
+ 'group' => nil,
+ 'group_id' => nil)
+
+ expect(milestone.persisted?).to be true
+ expect(Milestone.count).to eq(2)
+ expect(milestone.iid).to eq(clashing_iid)
+ end
+ end
end
context 'merge_request' do
@@ -176,4 +194,118 @@ RSpec.describe Gitlab::ImportExport::Project::ObjectBuilder do
expect(found.email).to eq('alice@example.com')
end
end
+
+ context 'merge request diff commits' do
+ context 'when the "committer" object is present' do
+ it 'uses this object as the committer' do
+ user = MergeRequest::DiffCommitUser
+ .find_or_create('Alice', 'alice@example.com')
+
+ commit = described_class.build(
+ MergeRequestDiffCommit,
+ {
+ 'committer' => user,
+ 'committer_name' => 'Bla',
+ 'committer_email' => 'bla@example.com',
+ 'author_name' => 'Bla',
+ 'author_email' => 'bla@example.com'
+ }
+ )
+
+ expect(commit.committer).to eq(user)
+ end
+ end
+
+ context 'when the "committer" object is missing' do
+ it 'creates one from the committer name and Email' do
+ commit = described_class.build(
+ MergeRequestDiffCommit,
+ {
+ 'committer_name' => 'Alice',
+ 'committer_email' => 'alice@example.com',
+ 'author_name' => 'Alice',
+ 'author_email' => 'alice@example.com'
+ }
+ )
+
+ expect(commit.committer.name).to eq('Alice')
+ expect(commit.committer.email).to eq('alice@example.com')
+ end
+ end
+
+ context 'when the "commit_author" object is present' do
+ it 'uses this object as the author' do
+ user = MergeRequest::DiffCommitUser
+ .find_or_create('Alice', 'alice@example.com')
+
+ commit = described_class.build(
+ MergeRequestDiffCommit,
+ {
+ 'committer_name' => 'Alice',
+ 'committer_email' => 'alice@example.com',
+ 'commit_author' => user,
+ 'author_name' => 'Bla',
+ 'author_email' => 'bla@example.com'
+ }
+ )
+
+ expect(commit.commit_author).to eq(user)
+ end
+ end
+
+ context 'when the "commit_author" object is missing' do
+ it 'creates one from the author name and Email' do
+ commit = described_class.build(
+ MergeRequestDiffCommit,
+ {
+ 'committer_name' => 'Alice',
+ 'committer_email' => 'alice@example.com',
+ 'author_name' => 'Alice',
+ 'author_email' => 'alice@example.com'
+ }
+ )
+
+ expect(commit.commit_author.name).to eq('Alice')
+ expect(commit.commit_author.email).to eq('alice@example.com')
+ end
+ end
+ end
+
+ describe '#find_or_create_diff_commit_user' do
+ context 'when the user already exists' do
+ it 'returns the existing user' do
+ user = MergeRequest::DiffCommitUser
+ .find_or_create('Alice', 'alice@example.com')
+
+ found = described_class
+ .new(MergeRequestDiffCommit, {})
+ .send(:find_or_create_diff_commit_user, user.name, user.email)
+
+ expect(found).to eq(user)
+ end
+ end
+
+ context 'when the user does not exist' do
+ it 'creates the user' do
+ found = described_class
+ .new(MergeRequestDiffCommit, {})
+ .send(:find_or_create_diff_commit_user, 'Alice', 'alice@example.com')
+
+ expect(found.name).to eq('Alice')
+ expect(found.email).to eq('alice@example.com')
+ end
+ end
+
+ it 'caches the results' do
+ builder = described_class.new(MergeRequestDiffCommit, {})
+
+ builder.send(:find_or_create_diff_commit_user, 'Alice', 'alice@example.com')
+
+ record = ActiveRecord::QueryRecorder.new do
+ builder.send(:find_or_create_diff_commit_user, 'Alice', 'alice@example.com')
+ end
+
+ expect(record.count).to eq(1)
+ end
+ end
end
diff --git a/spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb
new file mode 100644
index 00000000000..5ebace263ba
--- /dev/null
+++ b/spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb
@@ -0,0 +1,150 @@
+# frozen_string_literal: true
+
+# This spec is a lightweight version of:
+# * project/tree_restorer_spec.rb
+#
+# In depth testing is being done in the above specs.
+# This spec tests that restore project works
+# but does not have 100% relation coverage.
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::ImportExport::Project::RelationTreeRestorer do
+ let_it_be(:importable, reload: true) do
+ create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project')
+ end
+
+ include_context 'relation tree restorer shared context' do
+ let(:importable_name) { 'project' }
+ end
+
+ let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) }
+ let(:relation_tree_restorer) do
+ described_class.new(
+ user: user,
+ shared: shared,
+ relation_reader: relation_reader,
+ object_builder: Gitlab::ImportExport::Project::ObjectBuilder,
+ members_mapper: members_mapper,
+ relation_factory: Gitlab::ImportExport::Project::RelationFactory,
+ reader: reader,
+ importable: importable,
+ importable_path: 'project',
+ importable_attributes: attributes
+ )
+ end
+
+ subject { relation_tree_restorer.restore }
+
+ shared_examples 'import project successfully' do
+ describe 'imported project' do
+ it 'has the project attributes and relations', :aggregate_failures do
+ expect(subject).to eq(true)
+
+ project = Project.find_by_path('project')
+
+ expect(project.description).to eq('Nisi et repellendus ut enim quo accusamus vel magnam.')
+ expect(project.labels.count).to eq(3)
+ expect(project.boards.count).to eq(1)
+ expect(project.project_feature).not_to be_nil
+ expect(project.custom_attributes.count).to eq(2)
+ expect(project.project_badges.count).to eq(2)
+ expect(project.snippets.count).to eq(1)
+ end
+ end
+ end
+
+ shared_examples 'logging of relations creation' do
+ context 'when log_import_export_relation_creation feature flag is enabled' do
+ before do
+ stub_feature_flags(log_import_export_relation_creation: group)
+ end
+
+ it 'logs top-level relation creation' do
+ expect(shared.logger)
+ .to receive(:info)
+ .with(hash_including(message: '[Project/Group Import] Created new object relation'))
+ .at_least(:once)
+
+ subject
+ end
+ end
+
+ context 'when log_import_export_relation_creation feature flag is disabled' do
+ before do
+ stub_feature_flags(log_import_export_relation_creation: false)
+ end
+
+ it 'does not log top-level relation creation' do
+ expect(shared.logger)
+ .to receive(:info)
+ .with(hash_including(message: '[Project/Group Import] Created new object relation'))
+ .never
+
+ subject
+ end
+ end
+ end
+
+ context 'with legacy reader' do
+ let(:path) { 'spec/fixtures/lib/gitlab/import_export/complex/project.json' }
+ let(:relation_reader) do
+ Gitlab::ImportExport::Json::LegacyReader::File.new(
+ path,
+ relation_names: reader.project_relation_names,
+ allowed_path: 'project'
+ )
+ end
+
+ let(:attributes) { relation_reader.consume_attributes('project') }
+
+ it_behaves_like 'import project successfully'
+
+ context 'with logging of relations creation' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:importable) do
+ create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project', group: group)
+ end
+
+ include_examples 'logging of relations creation'
+ end
+ end
+
+ context 'with ndjson reader' do
+ let(:path) { 'spec/fixtures/lib/gitlab/import_export/complex/tree' }
+ let(:relation_reader) { Gitlab::ImportExport::Json::NdjsonReader.new(path) }
+
+ it_behaves_like 'import project successfully'
+
+ context 'when inside a group' do
+ let_it_be(:group) do
+ create(:group, :disabled_and_unoverridable)
+ end
+
+ before do
+ importable.update!(shared_runners_enabled: false, group: group)
+ end
+
+ it_behaves_like 'import project successfully'
+ end
+ end
+
+ context 'with invalid relations' do
+ let(:path) { 'spec/fixtures/lib/gitlab/import_export/project_with_invalid_relations/tree' }
+ let(:relation_reader) { Gitlab::ImportExport::Json::NdjsonReader.new(path) }
+
+ it 'logs the invalid relation and its errors' do
+ expect(shared.logger)
+ .to receive(:warn)
+ .with(
+ error_messages: "Title can't be blank. Title is invalid",
+ message: '[Project/Group Import] Invalid object relation built',
+ relation_class: 'ProjectLabel',
+ relation_index: 0,
+ relation_key: 'labels'
+ ).once
+
+ relation_tree_restorer.restore
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/project/sample/relation_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/sample/relation_tree_restorer_spec.rb
index f6a028383f2..3dab84af744 100644
--- a/spec/lib/gitlab/import_export/project/sample/relation_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project/sample/relation_tree_restorer_spec.rb
@@ -10,19 +10,26 @@
require 'spec_helper'
RSpec.describe Gitlab::ImportExport::Project::Sample::RelationTreeRestorer do
- include_context 'relation tree restorer shared context'
+ let_it_be(:importable) { create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project') }
+ include_context 'relation tree restorer shared context' do
+ let(:importable_name) { 'project' }
+ end
+
+ let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) }
+ let(:path) { 'spec/fixtures/lib/gitlab/import_export/sample_data/tree' }
+ let(:relation_reader) { Gitlab::ImportExport::Json::NdjsonReader.new(path) }
let(:sample_data_relation_tree_restorer) do
described_class.new(
user: user,
shared: shared,
relation_reader: relation_reader,
- object_builder: object_builder,
+ object_builder: Gitlab::ImportExport::Project::ObjectBuilder,
members_mapper: members_mapper,
- relation_factory: relation_factory,
+ relation_factory: Gitlab::ImportExport::Project::Sample::RelationFactory,
reader: reader,
importable: importable,
- importable_path: importable_path,
+ importable_path: 'project',
importable_attributes: attributes
)
end
@@ -69,32 +76,21 @@ RSpec.describe Gitlab::ImportExport::Project::Sample::RelationTreeRestorer do
end
end
- context 'when restoring a project' do
- let(:importable) { create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project') }
- let(:importable_name) { 'project' }
- let(:importable_path) { 'project' }
- let(:object_builder) { Gitlab::ImportExport::Project::ObjectBuilder }
- let(:relation_factory) { Gitlab::ImportExport::Project::Sample::RelationFactory }
- let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) }
- let(:path) { 'spec/fixtures/lib/gitlab/import_export/sample_data/tree' }
- let(:relation_reader) { Gitlab::ImportExport::Json::NdjsonReader.new(path) }
-
- it 'initializes relation_factory with date_calculator as parameter' do
- expect(Gitlab::ImportExport::Project::Sample::RelationFactory).to receive(:create).with(hash_including(:date_calculator)).at_least(:once).times
+ it 'initializes relation_factory with date_calculator as parameter' do
+ expect(Gitlab::ImportExport::Project::Sample::RelationFactory).to receive(:create).with(hash_including(:date_calculator)).at_least(:once).times
- subject
- end
+ subject
+ end
- context 'when relation tree restorer is initialized' do
- it 'initializes date calculator with due dates' do
- expect(Gitlab::ImportExport::Project::Sample::DateCalculator).to receive(:new).with(Array)
+ context 'when relation tree restorer is initialized' do
+ it 'initializes date calculator with due dates' do
+ expect(Gitlab::ImportExport::Project::Sample::DateCalculator).to receive(:new).with(Array)
- sample_data_relation_tree_restorer
- end
+ sample_data_relation_tree_restorer
end
+ end
- context 'using ndjson reader' do
- it_behaves_like 'import project successfully'
- end
+ context 'using ndjson reader' do
+ it_behaves_like 'import project successfully'
end
end
diff --git a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
index f512f49764d..cd3d29f1a51 100644
--- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do
]
RSpec::Mocks.with_temporary_scope do
- @project = create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project')
+ @project = create(:project, :repository, :builds_enabled, :issues_disabled, name: 'project', path: 'project')
@shared = @project.import_export_shared
stub_all_feature_flags
@@ -36,7 +36,6 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do
allow_any_instance_of(Gitlab::Git::Repository).to receive(:branch_exists?).and_return(false)
expect(@shared).not_to receive(:error)
- expect_any_instance_of(Gitlab::Git::Repository).to receive(:create_branch).with('feature', 'DCBA')
allow_any_instance_of(Gitlab::Git::Repository).to receive(:create_branch)
project_tree_restorer = described_class.new(user: @user, shared: @shared, project: @project)
diff --git a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb
index 374d688576e..f68ec21039d 100644
--- a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb
@@ -5,6 +5,9 @@ require 'spec_helper'
RSpec.describe Gitlab::ImportExport::Project::TreeSaver do
let_it_be(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let_it_be(:exportable_path) { 'project' }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { setup_project }
shared_examples 'saves project tree successfully' do |ndjson_enabled|
include ImportExport::CommonUtil
@@ -12,9 +15,6 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver do
subject { get_json(full_path, exportable_path, relation_name, ndjson_enabled) }
describe 'saves project tree attributes' do
- let_it_be(:user) { create(:user) }
- let_it_be(:group) { create(:group) }
- let_it_be(:project) { setup_project }
let_it_be(:shared) { project.import_export_shared }
let(:relation_name) { :projects }
@@ -402,6 +402,50 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver do
it_behaves_like "saves project tree successfully", true
end
+ context 'when streaming has to retry', :aggregate_failures do
+ let(:shared) { double('shared', export_path: exportable_path) }
+ let(:logger) { Gitlab::Import::Logger.build }
+ let(:serializer) { double('serializer') }
+ let(:error_class) { Net::OpenTimeout }
+ let(:info_params) do
+ {
+ 'error.class': error_class,
+ project_name: project.name,
+ project_id: project.id
+ }
+ end
+
+ before do
+ allow(Gitlab::ImportExport::Json::StreamingSerializer).to receive(:new).and_return(serializer)
+ end
+
+ subject(:project_tree_saver) do
+ described_class.new(project: project, current_user: user, shared: shared, logger: logger)
+ end
+
+ it 'retries and succeeds' do
+ call_count = 0
+ allow(serializer).to receive(:execute) do
+ call_count += 1
+ call_count > 1 ? true : raise(error_class, 'execution expired')
+ end
+
+ expect(logger).to receive(:info).with(hash_including(info_params)).once
+
+ expect(project_tree_saver.save).to be(true)
+ end
+
+ it 'retries and does not succeed' do
+ retry_count = 3
+ allow(serializer).to receive(:execute).and_raise(error_class, 'execution expired')
+
+ expect(logger).to receive(:info).with(hash_including(info_params)).exactly(retry_count).times
+ expect(shared).to receive(:error).with(instance_of(error_class))
+
+ expect(project_tree_saver.save).to be(false)
+ end
+ end
+
def setup_project
release = create(:release)
diff --git a/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb
deleted file mode 100644
index 5e4075c2b59..00000000000
--- a/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb
+++ /dev/null
@@ -1,184 +0,0 @@
-# frozen_string_literal: true
-
-# This spec is a lightweight version of:
-# * project/tree_restorer_spec.rb
-#
-# In depth testing is being done in the above specs.
-# This spec tests that restore project works
-# but does not have 100% relation coverage.
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::ImportExport::RelationTreeRestorer do
- include_context 'relation tree restorer shared context'
-
- let(:relation_tree_restorer) do
- described_class.new(
- user: user,
- shared: shared,
- relation_reader: relation_reader,
- object_builder: object_builder,
- members_mapper: members_mapper,
- relation_factory: relation_factory,
- reader: reader,
- importable: importable,
- importable_path: importable_path,
- importable_attributes: attributes
- )
- end
-
- subject { relation_tree_restorer.restore }
-
- shared_examples 'import project successfully' do
- describe 'imported project' do
- it 'has the project attributes and relations', :aggregate_failures do
- expect(subject).to eq(true)
-
- project = Project.find_by_path('project')
-
- expect(project.description).to eq('Nisi et repellendus ut enim quo accusamus vel magnam.')
- expect(project.labels.count).to eq(3)
- expect(project.boards.count).to eq(1)
- expect(project.project_feature).not_to be_nil
- expect(project.custom_attributes.count).to eq(2)
- expect(project.project_badges.count).to eq(2)
- expect(project.snippets.count).to eq(1)
- end
- end
- end
-
- shared_examples 'logging of relations creation' do
- context 'when log_import_export_relation_creation feature flag is enabled' do
- before do
- stub_feature_flags(log_import_export_relation_creation: group)
- end
-
- it 'logs top-level relation creation' do
- expect(relation_tree_restorer.shared.logger)
- .to receive(:info)
- .with(hash_including(message: '[Project/Group Import] Created new object relation'))
- .at_least(:once)
-
- subject
- end
- end
-
- context 'when log_import_export_relation_creation feature flag is disabled' do
- before do
- stub_feature_flags(log_import_export_relation_creation: false)
- end
-
- it 'does not log top-level relation creation' do
- expect(relation_tree_restorer.shared.logger)
- .to receive(:info)
- .with(hash_including(message: '[Project/Group Import] Created new object relation'))
- .never
-
- subject
- end
- end
- end
-
- context 'when restoring a project' do
- let_it_be(:importable, reload: true) do
- create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project')
- end
-
- let(:importable_name) { 'project' }
- let(:importable_path) { 'project' }
- let(:object_builder) { Gitlab::ImportExport::Project::ObjectBuilder }
- let(:relation_factory) { Gitlab::ImportExport::Project::RelationFactory }
- let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) }
-
- context 'using legacy reader' do
- let(:path) { 'spec/fixtures/lib/gitlab/import_export/complex/project.json' }
- let(:relation_reader) do
- Gitlab::ImportExport::Json::LegacyReader::File.new(
- path,
- relation_names: reader.project_relation_names,
- allowed_path: 'project'
- )
- end
-
- let(:attributes) { relation_reader.consume_attributes('project') }
-
- it_behaves_like 'import project successfully'
-
- context 'logging of relations creation' do
- let_it_be(:group) { create(:group) }
- let_it_be(:importable) do
- create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project', group: group)
- end
-
- include_examples 'logging of relations creation'
- end
- end
-
- context 'using ndjson reader' do
- let(:path) { 'spec/fixtures/lib/gitlab/import_export/complex/tree' }
- let(:relation_reader) { Gitlab::ImportExport::Json::NdjsonReader.new(path) }
-
- it_behaves_like 'import project successfully'
-
- context 'when inside a group' do
- let_it_be(:group) do
- create(:group, :disabled_and_unoverridable)
- end
-
- before do
- importable.update!(shared_runners_enabled: false, group: group)
- end
-
- it_behaves_like 'import project successfully'
- end
- end
-
- context 'with invalid relations' do
- let(:path) { 'spec/fixtures/lib/gitlab/import_export/project_with_invalid_relations/tree' }
- let(:relation_reader) { Gitlab::ImportExport::Json::NdjsonReader.new(path) }
-
- it 'logs the invalid relation and its errors' do
- expect(relation_tree_restorer.shared.logger)
- .to receive(:warn)
- .with(
- error_messages: "Title can't be blank. Title is invalid",
- message: '[Project/Group Import] Invalid object relation built',
- relation_class: 'ProjectLabel',
- relation_index: 0,
- relation_key: 'labels'
- ).once
-
- relation_tree_restorer.restore
- end
- end
- end
-
- context 'when restoring a group' do
- let_it_be(:group) { create(:group) }
- let_it_be(:importable) { create(:group, parent: group) }
-
- let(:path) { 'spec/fixtures/lib/gitlab/import_export/group_exports/no_children/group.json' }
- let(:importable_name) { nil }
- let(:importable_path) { nil }
- let(:object_builder) { Gitlab::ImportExport::Group::ObjectBuilder }
- let(:relation_factory) { Gitlab::ImportExport::Group::RelationFactory }
- let(:relation_reader) do
- Gitlab::ImportExport::Json::LegacyReader::File.new(
- path,
- relation_names: reader.group_relation_names)
- end
-
- let(:reader) do
- Gitlab::ImportExport::Reader.new(
- shared: shared,
- config: Gitlab::ImportExport::Config.new(config: Gitlab::ImportExport.legacy_group_config_file).to_h
- )
- end
-
- it 'restores group tree' do
- expect(subject).to eq(true)
- end
-
- include_examples 'logging of relations creation'
- end
-end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 4b125cab49b..9daa3b32fd1 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -561,6 +561,7 @@ Project:
- require_password_to_approve
- autoclose_referenced_issues
- suggestion_commit_message
+- merge_commit_template
ProjectTracingSetting:
- external_url
Author:
@@ -692,6 +693,7 @@ ProjectCiCdSetting:
ProjectSetting:
- allow_merge_on_skipped_pipeline
- has_confluence
+- has_shimo
- has_vulnerabilities
ProtectedEnvironment:
- id
diff --git a/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb b/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb
index b2a11353d0c..09280402e2b 100644
--- a/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb
+++ b/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb
@@ -111,45 +111,4 @@ RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_sh
end
end
end
-
- context 'when a command takes longer than DURATION_ERROR_THRESHOLD' do
- let(:threshold) { 0.5 }
-
- before do
- stub_const("#{described_class}::DURATION_ERROR_THRESHOLD", threshold)
- end
-
- context 'when report_on_long_redis_durations is disabled' do
- it 'does nothing' do
- stub_feature_flags(report_on_long_redis_durations: false)
-
- expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
-
- Gitlab::Redis::SharedState.with { |r| r.mget('foo', 'foo') { sleep threshold + 0.1 } }
- end
- end
-
- context 'when report_on_long_redis_durations is enabled' do
- context 'for an instance other than SharedState' do
- it 'does nothing' do
- expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
-
- Gitlab::Redis::Queues.with { |r| r.mget('foo', 'foo') { sleep threshold + 0.1 } }
- end
- end
-
- context 'for the SharedState instance' do
- it 'tracks an exception and continues' do
- expect(Gitlab::ErrorTracking)
- .to receive(:track_exception)
- .with(an_instance_of(described_class::MysteryRedisDurationError),
- command: 'mget',
- duration: be > threshold,
- timestamp: a_string_matching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{5}/))
-
- Gitlab::Redis::SharedState.with { |r| r.mget('foo', 'foo') { sleep threshold + 0.1 } }
- end
- end
- end
- end
end
diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb
index 52d3623c304..a9663012e9a 100644
--- a/spec/lib/gitlab/instrumentation_helper_spec.rb
+++ b/spec/lib/gitlab/instrumentation_helper_spec.rb
@@ -147,6 +147,25 @@ RSpec.describe Gitlab::InstrumentationHelper do
expect(payload).not_to include(:caught_up_replica_pick_fail)
end
end
+
+ context 'when there is an uploaded file' do
+ it 'adds upload data' do
+ uploaded_file = UploadedFile.from_params({
+ 'name' => 'dir/foo.txt',
+ 'sha256' => 'sha256',
+ 'remote_url' => 'http://localhost/file',
+ 'remote_id' => '1234567890',
+ 'etag' => 'etag1234567890',
+ 'upload_duration' => '5.05',
+ 'size' => '123456'
+ }, nil)
+
+ subject
+
+ expect(payload[:uploaded_file_upload_duration_s]).to eq(uploaded_file.upload_duration)
+ expect(payload[:uploaded_file_size_bytes]).to eq(uploaded_file.size)
+ end
+ end
end
describe 'duration calculations' do
diff --git a/spec/lib/gitlab/issues/rebalancing/state_spec.rb b/spec/lib/gitlab/issues/rebalancing/state_spec.rb
index bdd0dbd365d..a849330ad35 100644
--- a/spec/lib/gitlab/issues/rebalancing/state_spec.rb
+++ b/spec/lib/gitlab/issues/rebalancing/state_spec.rb
@@ -94,7 +94,7 @@ RSpec.describe Gitlab::Issues::Rebalancing::State, :clean_gitlab_redis_shared_st
context 'when tracking new rebalance' do
it 'returns as expired for non existent key' do
::Gitlab::Redis::SharedState.with do |redis|
- expect(redis.ttl(rebalance_caching.send(:concurrent_running_rebalances_key))).to be < 0
+ expect(redis.ttl(Gitlab::Issues::Rebalancing::State::CONCURRENT_RUNNING_REBALANCES_KEY)).to be < 0
end
end
@@ -102,7 +102,7 @@ RSpec.describe Gitlab::Issues::Rebalancing::State, :clean_gitlab_redis_shared_st
rebalance_caching.track_new_running_rebalance
::Gitlab::Redis::SharedState.with do |redis|
- expect(redis.ttl(rebalance_caching.send(:concurrent_running_rebalances_key))).to be_between(0, described_class::REDIS_EXPIRY_TIME.ago.to_i)
+ expect(redis.ttl(Gitlab::Issues::Rebalancing::State::CONCURRENT_RUNNING_REBALANCES_KEY)).to be_between(0, described_class::REDIS_EXPIRY_TIME.ago.to_i)
end
end
end
@@ -169,7 +169,7 @@ RSpec.describe Gitlab::Issues::Rebalancing::State, :clean_gitlab_redis_shared_st
rebalance_caching.cleanup_cache
- expect(check_existing_keys).to eq(0)
+ expect(check_existing_keys).to eq(1)
end
end
end
@@ -183,6 +183,16 @@ RSpec.describe Gitlab::Issues::Rebalancing::State, :clean_gitlab_redis_shared_st
it { expect(rebalance_caching.send(:rebalanced_container_type)).to eq(described_class::NAMESPACE) }
it_behaves_like 'issues rebalance caching'
+
+ describe '.fetch_rebalancing_groups_and_projects' do
+ before do
+ rebalance_caching.track_new_running_rebalance
+ end
+
+ it 'caches recently finished rebalance key' do
+ expect(described_class.fetch_rebalancing_groups_and_projects).to eq([[group.id], []])
+ end
+ end
end
context 'rebalancing issues in a project' do
@@ -193,6 +203,16 @@ RSpec.describe Gitlab::Issues::Rebalancing::State, :clean_gitlab_redis_shared_st
it { expect(rebalance_caching.send(:rebalanced_container_type)).to eq(described_class::PROJECT) }
it_behaves_like 'issues rebalance caching'
+
+ describe '.fetch_rebalancing_groups_and_projects' do
+ before do
+ rebalance_caching.track_new_running_rebalance
+ end
+
+ it 'caches recently finished rebalance key' do
+ expect(described_class.fetch_rebalancing_groups_and_projects).to eq([[], [project.id]])
+ end
+ end
end
# count - how many issue ids to generate, issue ids will start at 1
@@ -212,11 +232,14 @@ RSpec.describe Gitlab::Issues::Rebalancing::State, :clean_gitlab_redis_shared_st
def check_existing_keys
index = 0
+ # spec only, we do not actually scan keys in the code
+ recently_finished_keys_count = Gitlab::Redis::SharedState.with { |redis| redis.scan(0, match: "#{described_class::RECENTLY_FINISHED_REBALANCE_PREFIX}:*") }.last.count
index += 1 if rebalance_caching.get_current_index > 0
index += 1 if rebalance_caching.get_current_project_id.present?
index += 1 if rebalance_caching.get_cached_issue_ids(0, 100).present?
index += 1 if rebalance_caching.rebalance_in_progress?
+ index += 1 if recently_finished_keys_count > 0
index
end
diff --git a/spec/lib/gitlab/lograge/custom_options_spec.rb b/spec/lib/gitlab/lograge/custom_options_spec.rb
index 9daedfc37e4..a4ae39a835a 100644
--- a/spec/lib/gitlab/lograge/custom_options_spec.rb
+++ b/spec/lib/gitlab/lograge/custom_options_spec.rb
@@ -19,7 +19,13 @@ RSpec.describe Gitlab::Lograge::CustomOptions do
user_id: 'test',
cf_ray: SecureRandom.hex,
cf_request_id: SecureRandom.hex,
- metadata: { 'meta.user' => 'jane.doe' }
+ metadata: { 'meta.user' => 'jane.doe' },
+ request_urgency: :default,
+ target_duration_s: 1,
+ remote_ip: '192.168.1.2',
+ ua: 'Nyxt',
+ queue_duration_s: 0.2,
+ etag_route: '/etag'
}
end
@@ -66,6 +72,18 @@ RSpec.describe Gitlab::Lograge::CustomOptions do
end
end
+ context 'trusted payload' do
+ it { is_expected.to include(event_payload.slice(*described_class::KNOWN_PAYLOAD_PARAMS)) }
+
+ context 'payload with rejected fields' do
+ let(:event_payload) { { params: {}, request_urgency: :high, something: 'random', username: nil } }
+
+ it { is_expected.to include({ request_urgency: :high }) }
+ it { is_expected.not_to include({ something: 'random' }) }
+ it { is_expected.not_to include({ username: nil }) }
+ end
+ end
+
context 'when correlation_id is overridden' do
let(:correlation_id_key) { Labkit::Correlation::CorrelationId::LOG_KEY }
diff --git a/spec/lib/gitlab/merge_requests/merge_commit_message_spec.rb b/spec/lib/gitlab/merge_requests/merge_commit_message_spec.rb
new file mode 100644
index 00000000000..884f8df5e56
--- /dev/null
+++ b/spec/lib/gitlab/merge_requests/merge_commit_message_spec.rb
@@ -0,0 +1,219 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::MergeRequests::MergeCommitMessage do
+ let(:merge_commit_template) { nil }
+ let(:project) { create(:project, :public, :repository, merge_commit_template: merge_commit_template) }
+ let(:user) { project.creator }
+ let(:merge_request_description) { "Merge Request Description\nNext line" }
+ let(:merge_request_title) { 'Bugfix' }
+ let(:merge_request) do
+ create(
+ :merge_request,
+ :simple,
+ source_project: project,
+ target_project: project,
+ author: user,
+ description: merge_request_description,
+ title: merge_request_title
+ )
+ end
+
+ subject { described_class.new(merge_request: merge_request) }
+
+ it 'returns nil when template is not set in target project' do
+ expect(subject.message).to be_nil
+ end
+
+ context 'when project has custom merge commit template' do
+ let(:merge_commit_template) { <<~MSG.rstrip }
+ %{title}
+
+ See merge request %{reference}
+ MSG
+
+ it 'uses custom template' do
+ expect(subject.message).to eq <<~MSG.rstrip
+ Bugfix
+
+ See merge request #{merge_request.to_reference(full: true)}
+ MSG
+ end
+ end
+
+ context 'when project has merge commit template with closed issues' do
+ let(:merge_commit_template) { <<~MSG.rstrip }
+ Merge branch '%{source_branch}' into '%{target_branch}'
+
+ %{title}
+
+ %{issues}
+
+ See merge request %{reference}
+ MSG
+
+ it 'omits issues and new lines when no issues are mentioned in description' do
+ expect(subject.message).to eq <<~MSG.rstrip
+ Merge branch 'feature' into 'master'
+
+ Bugfix
+
+ See merge request #{merge_request.to_reference(full: true)}
+ MSG
+ end
+
+ context 'when MR closes issues' do
+ let(:issue_1) { create(:issue, project: project) }
+ let(:issue_2) { create(:issue, project: project) }
+ let(:merge_request_description) { "Description\n\nclosing #{issue_1.to_reference}, #{issue_2.to_reference}" }
+
+ it 'includes them and keeps new line characters' do
+ expect(subject.message).to eq <<~MSG.rstrip
+ Merge branch 'feature' into 'master'
+
+ Bugfix
+
+ Closes #{issue_1.to_reference} and #{issue_2.to_reference}
+
+ See merge request #{merge_request.to_reference(full: true)}
+ MSG
+ end
+ end
+ end
+
+ context 'when project has merge commit template with description' do
+ let(:merge_commit_template) { <<~MSG.rstrip }
+ Merge branch '%{source_branch}' into '%{target_branch}'
+
+ %{title}
+
+ %{description}
+
+ See merge request %{reference}
+ MSG
+
+ it 'uses template' do
+ expect(subject.message).to eq <<~MSG.rstrip
+ Merge branch 'feature' into 'master'
+
+ Bugfix
+
+ Merge Request Description
+ Next line
+
+ See merge request #{merge_request.to_reference(full: true)}
+ MSG
+ end
+
+ context 'when description is empty string' do
+ let(:merge_request_description) { '' }
+
+ it 'skips description placeholder and removes new line characters before it' do
+ expect(subject.message).to eq <<~MSG.rstrip
+ Merge branch 'feature' into 'master'
+
+ Bugfix
+
+ See merge request #{merge_request.to_reference(full: true)}
+ MSG
+ end
+ end
+
+ context 'when description is nil' do
+ let(:merge_request_description) { nil }
+
+ it 'skips description placeholder and removes new line characters before it' do
+ expect(subject.message).to eq <<~MSG.rstrip
+ Merge branch 'feature' into 'master'
+
+ Bugfix
+
+ See merge request #{merge_request.to_reference(full: true)}
+ MSG
+ end
+ end
+
+ context 'when description is blank string' do
+ let(:merge_request_description) { "\n\r \n" }
+
+ it 'skips description placeholder and removes new line characters before it' do
+ expect(subject.message).to eq <<~MSG.rstrip
+ Merge branch 'feature' into 'master'
+
+ Bugfix
+
+ See merge request #{merge_request.to_reference(full: true)}
+ MSG
+ end
+ end
+ end
+
+ context 'when custom merge commit template contains placeholder in the middle or beginning of the line' do
+ let(:merge_commit_template) { <<~MSG.rstrip }
+ Merge branch '%{source_branch}' into '%{target_branch}'
+
+ %{description} %{title}
+
+ See merge request %{reference}
+ MSG
+
+ it 'uses custom template' do
+ expect(subject.message).to eq <<~MSG.rstrip
+ Merge branch 'feature' into 'master'
+
+ Merge Request Description
+ Next line Bugfix
+
+ See merge request #{merge_request.to_reference(full: true)}
+ MSG
+ end
+
+ context 'when description is empty string' do
+ let(:merge_request_description) { '' }
+
+ it 'does not remove new line characters before empty placeholder' do
+ expect(subject.message).to eq <<~MSG.rstrip
+ Merge branch 'feature' into 'master'
+
+ Bugfix
+
+ See merge request #{merge_request.to_reference(full: true)}
+ MSG
+ end
+ end
+ end
+
+ context 'when project has template with CRLF newlines' do
+ let(:merge_commit_template) do
+ "Merge branch '%{source_branch}' into '%{target_branch}'\r\n\r\n%{title}\r\n\r\n%{description}\r\n\r\nSee merge request %{reference}"
+ end
+
+ it 'converts it to LF newlines' do
+ expect(subject.message).to eq <<~MSG.rstrip
+ Merge branch 'feature' into 'master'
+
+ Bugfix
+
+ Merge Request Description
+ Next line
+
+ See merge request #{merge_request.to_reference(full: true)}
+ MSG
+ end
+
+ context 'when description is empty string' do
+ let(:merge_request_description) { '' }
+
+ it 'skips description placeholder and removes new line characters before it' do
+ expect(subject.message).to eq <<~MSG.rstrip
+ Merge branch 'feature' into 'master'
+
+ Bugfix
+
+ See merge request #{merge_request.to_reference(full: true)}
+ MSG
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/background_transaction_spec.rb b/spec/lib/gitlab/metrics/background_transaction_spec.rb
index d36ee24fc50..83bee84df99 100644
--- a/spec/lib/gitlab/metrics/background_transaction_spec.rb
+++ b/spec/lib/gitlab/metrics/background_transaction_spec.rb
@@ -4,27 +4,28 @@ require 'spec_helper'
RSpec.describe Gitlab::Metrics::BackgroundTransaction do
let(:transaction) { described_class.new }
- let(:prometheus_metric) { instance_double(Prometheus::Client::Metric, base_labels: {}) }
-
- before do
- allow(described_class).to receive(:prometheus_metric).and_return(prometheus_metric)
- end
describe '#run' do
+ let(:prometheus_metric) { instance_double(Prometheus::Client::Metric, base_labels: {}) }
+
+ before do
+ allow(described_class).to receive(:prometheus_metric).and_return(prometheus_metric)
+ end
+
it 'yields the supplied block' do
expect { |b| transaction.run(&b) }.to yield_control
end
it 'stores the transaction in the current thread' do
transaction.run do
- expect(Thread.current[described_class::BACKGROUND_THREAD_KEY]).to eq(transaction)
+ expect(Thread.current[described_class::THREAD_KEY]).to eq(transaction)
end
end
it 'removes the transaction from the current thread upon completion' do
transaction.run { }
- expect(Thread.current[described_class::BACKGROUND_THREAD_KEY]).to be_nil
+ expect(Thread.current[described_class::THREAD_KEY]).to be_nil
end
end
@@ -68,7 +69,10 @@ RSpec.describe Gitlab::Metrics::BackgroundTransaction do
end
end
- RSpec.shared_examples 'metric with labels' do |metric_method|
+ it_behaves_like 'transaction metrics with labels' do
+ let(:transaction_obj) { described_class.new }
+ let(:labels) { { endpoint_id: 'TestWorker', feature_category: 'projects', queue: 'test_worker' } }
+
before do
test_worker_class = Class.new do
def self.queue
@@ -78,33 +82,10 @@ RSpec.describe Gitlab::Metrics::BackgroundTransaction do
stub_const('TestWorker', test_worker_class)
end
- it 'measures with correct labels and value' do
- value = 1
- expect(prometheus_metric).to receive(metric_method).with({
- endpoint_id: 'TestWorker', feature_category: 'projects', queue: 'test_worker'
- }, value)
-
+ around do |example|
Gitlab::ApplicationContext.with_raw_context(feature_category: 'projects', caller_id: 'TestWorker') do
- transaction.send(metric_method, :test_metric, value)
+ example.run
end
end
end
-
- describe '#increment' do
- let(:prometheus_metric) { instance_double(Prometheus::Client::Counter, :increment, base_labels: {}) }
-
- it_behaves_like 'metric with labels', :increment
- end
-
- describe '#set' do
- let(:prometheus_metric) { instance_double(Prometheus::Client::Gauge, :set, base_labels: {}) }
-
- it_behaves_like 'metric with labels', :set
- end
-
- describe '#observe' do
- let(:prometheus_metric) { instance_double(Prometheus::Client::Histogram, :observe, base_labels: {}) }
-
- it_behaves_like 'metric with labels', :observe
- end
end
diff --git a/spec/lib/gitlab/metrics/method_call_spec.rb b/spec/lib/gitlab/metrics/method_call_spec.rb
index fb5436a90e3..6aa89c7cb05 100644
--- a/spec/lib/gitlab/metrics/method_call_spec.rb
+++ b/spec/lib/gitlab/metrics/method_call_spec.rb
@@ -37,7 +37,7 @@ RSpec.describe Gitlab::Metrics::MethodCall do
it 'metric is not a NullMetric' do
method_call.measure { 'foo' }
- expect(::Gitlab::Metrics::Transaction.prometheus_metric(:gitlab_method_call_duration_seconds, :histogram)).not_to be_instance_of(Gitlab::Metrics::NullMetric)
+ expect(::Gitlab::Metrics::WebTransaction.prometheus_metric(:gitlab_method_call_duration_seconds, :histogram)).not_to be_instance_of(Gitlab::Metrics::NullMetric)
end
it 'observes the performance of the supplied block' do
@@ -63,7 +63,7 @@ RSpec.describe Gitlab::Metrics::MethodCall do
it 'observes using NullMetric' do
method_call.measure { 'foo' }
- expect(::Gitlab::Metrics::Transaction.prometheus_metric(:gitlab_method_call_duration_seconds, :histogram)).to be_instance_of(Gitlab::Metrics::NullMetric)
+ expect(::Gitlab::Metrics::WebTransaction.prometheus_metric(:gitlab_method_call_duration_seconds, :histogram)).to be_instance_of(Gitlab::Metrics::NullMetric)
end
end
end
diff --git a/spec/lib/gitlab/metrics/rails_slis_spec.rb b/spec/lib/gitlab/metrics/rails_slis_spec.rb
index 16fcb9d46a2..a5ccf7fafa4 100644
--- a/spec/lib/gitlab/metrics/rails_slis_spec.rb
+++ b/spec/lib/gitlab/metrics/rails_slis_spec.rb
@@ -10,49 +10,62 @@ RSpec.describe Gitlab::Metrics::RailsSlis do
allow(Gitlab::RequestEndpoints).to receive(:all_api_endpoints).and_return([api_route])
allow(Gitlab::RequestEndpoints).to receive(:all_controller_actions).and_return([[ProjectsController, 'show']])
+ allow(Gitlab::Graphql::KnownOperations).to receive(:default).and_return(Gitlab::Graphql::KnownOperations.new(%w(foo bar)))
end
describe '.initialize_request_slis_if_needed!' do
- it "initializes the SLI for all possible endpoints if they weren't" do
+ it "initializes the SLI for all possible endpoints if they weren't", :aggregate_failures do
possible_labels = [
{
endpoint_id: "GET /api/:version/version",
- feature_category: :not_owned
+ feature_category: :not_owned,
+ request_urgency: :default
},
{
endpoint_id: "ProjectsController#show",
- feature_category: :projects
+ feature_category: :projects,
+ request_urgency: :default
}
]
+ possible_graphql_labels = ['graphql:foo', 'graphql:bar', 'graphql:unknown', 'graphql:anonymous'].map do |endpoint_id|
+ {
+ endpoint_id: endpoint_id,
+ feature_category: nil,
+ query_urgency: ::Gitlab::EndpointAttributes::DEFAULT_URGENCY.name
+ }
+ end
+
expect(Gitlab::Metrics::Sli).to receive(:initialized?).with(:rails_request_apdex) { false }
+ expect(Gitlab::Metrics::Sli).to receive(:initialized?).with(:graphql_query_apdex) { false }
expect(Gitlab::Metrics::Sli).to receive(:initialize_sli).with(:rails_request_apdex, array_including(*possible_labels)).and_call_original
+ expect(Gitlab::Metrics::Sli).to receive(:initialize_sli).with(:graphql_query_apdex, array_including(*possible_graphql_labels)).and_call_original
described_class.initialize_request_slis_if_needed!
end
- it 'does not initialize the SLI if they were initialized already' do
+ it 'does not initialize the SLI if they were initialized already', :aggregate_failures do
expect(Gitlab::Metrics::Sli).to receive(:initialized?).with(:rails_request_apdex) { true }
+ expect(Gitlab::Metrics::Sli).to receive(:initialized?).with(:graphql_query_apdex) { true }
expect(Gitlab::Metrics::Sli).not_to receive(:initialize_sli)
described_class.initialize_request_slis_if_needed!
end
+ end
- it 'does not initialize anything if the feature flag is disabled' do
- stub_feature_flags(request_apdex_counters: false)
-
- expect(Gitlab::Metrics::Sli).not_to receive(:initialize_sli)
- expect(Gitlab::Metrics::Sli).not_to receive(:initialized?)
-
+ describe '.request_apdex' do
+ it 'returns the initialized request apdex SLI object' do
described_class.initialize_request_slis_if_needed!
+
+ expect(described_class.request_apdex).to be_initialized
end
end
- describe '.request_apdex' do
+ describe '.graphql_query_apdex' do
it 'returns the initialized request apdex SLI object' do
described_class.initialize_request_slis_if_needed!
- expect(described_class.request_apdex).to be_initialized
+ expect(described_class.graphql_query_apdex).to be_initialized
end
end
end
diff --git a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb
index 5870f9a8f68..3396de9b12c 100644
--- a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb
@@ -36,7 +36,8 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
it 'tracks request count and duration' do
expect(described_class).to receive_message_chain(:http_requests_total, :increment).with(method: 'get', status: '200', feature_category: 'unknown')
expect(described_class).to receive_message_chain(:http_request_duration_seconds, :observe).with({ method: 'get' }, a_positive_execution_time)
- expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment).with(labels: { feature_category: 'unknown', endpoint_id: 'unknown' }, success: true)
+ expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment)
+ .with(labels: { feature_category: 'unknown', endpoint_id: 'unknown', request_urgency: :default }, success: true)
subject.call(env)
end
@@ -115,14 +116,14 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
context 'application context' do
context 'when a context is present' do
before do
- ::Gitlab::ApplicationContext.push(feature_category: 'issue_tracking', caller_id: 'IssuesController#show')
+ ::Gitlab::ApplicationContext.push(feature_category: 'team_planning', caller_id: 'IssuesController#show')
end
it 'adds the feature category to the labels for required metrics' do
- expect(described_class).to receive_message_chain(:http_requests_total, :increment).with(method: 'get', status: '200', feature_category: 'issue_tracking')
+ expect(described_class).to receive_message_chain(:http_requests_total, :increment).with(method: 'get', status: '200', feature_category: 'team_planning')
expect(described_class).not_to receive(:http_health_requests_total)
expect(Gitlab::Metrics::RailsSlis.request_apdex)
- .to receive(:increment).with(labels: { feature_category: 'issue_tracking', endpoint_id: 'IssuesController#show' }, success: true)
+ .to receive(:increment).with(labels: { feature_category: 'team_planning', endpoint_id: 'IssuesController#show', request_urgency: :default }, success: true)
subject.call(env)
end
@@ -140,12 +141,12 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
context 'when application raises an exception when the feature category context is present' do
before do
- ::Gitlab::ApplicationContext.push(feature_category: 'issue_tracking')
+ ::Gitlab::ApplicationContext.push(feature_category: 'team_planning')
allow(app).to receive(:call).and_raise(StandardError)
end
it 'adds the feature category to the labels for http_requests_total' do
- expect(described_class).to receive_message_chain(:http_requests_total, :increment).with(method: 'get', status: 'undefined', feature_category: 'issue_tracking')
+ expect(described_class).to receive_message_chain(:http_requests_total, :increment).with(method: 'get', status: 'undefined', feature_category: 'team_planning')
expect(Gitlab::Metrics::RailsSlis).not_to receive(:request_apdex)
expect { subject.call(env) }.to raise_error(StandardError)
@@ -156,7 +157,8 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
it 'sets the required labels to unknown' do
expect(described_class).to receive_message_chain(:http_requests_total, :increment).with(method: 'get', status: '200', feature_category: 'unknown')
expect(described_class).not_to receive(:http_health_requests_total)
- expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment).with(labels: { feature_category: 'unknown', endpoint_id: 'unknown' }, success: true)
+ expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment)
+ .with(labels: { feature_category: 'unknown', endpoint_id: 'unknown', request_urgency: :default }, success: true)
subject.call(env)
end
@@ -206,7 +208,11 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
it "captures SLI metrics" do
expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment).with(
- labels: { feature_category: 'hello_world', endpoint_id: 'GET /projects/:id/archive' },
+ labels: {
+ feature_category: 'hello_world',
+ endpoint_id: 'GET /projects/:id/archive',
+ request_urgency: request_urgency_name
+ },
success: success
)
subject.call(env)
@@ -235,7 +241,11 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
it "captures SLI metrics" do
expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment).with(
- labels: { feature_category: 'hello_world', endpoint_id: 'AnonymousController#index' },
+ labels: {
+ feature_category: 'hello_world',
+ endpoint_id: 'AnonymousController#index',
+ request_urgency: request_urgency_name
+ },
success: success
)
subject.call(env)
@@ -255,17 +265,25 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
let(:api_handler) { Class.new(::API::Base) }
- it "falls back request's expectation to medium (1 second)" do
+ it "falls back request's expectation to default (1 second)" do
allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(100, 100.9)
expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment).with(
- labels: { feature_category: 'unknown', endpoint_id: 'unknown' },
+ labels: {
+ feature_category: 'unknown',
+ endpoint_id: 'unknown',
+ request_urgency: :default
+ },
success: true
)
subject.call(env)
allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(100, 101)
expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment).with(
- labels: { feature_category: 'unknown', endpoint_id: 'unknown' },
+ labels: {
+ feature_category: 'unknown',
+ endpoint_id: 'unknown',
+ request_urgency: :default
+ },
success: false
)
subject.call(env)
@@ -281,17 +299,25 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
{ 'action_controller.instance' => controller_instance, 'REQUEST_METHOD' => 'GET' }
end
- it "falls back request's expectation to medium (1 second)" do
+ it "falls back request's expectation to default (1 second)" do
allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(100, 100.9)
expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment).with(
- labels: { feature_category: 'unknown', endpoint_id: 'unknown' },
+ labels: {
+ feature_category: 'unknown',
+ endpoint_id: 'unknown',
+ request_urgency: :default
+ },
success: true
)
subject.call(env)
allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(100, 101)
expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment).with(
- labels: { feature_category: 'unknown', endpoint_id: 'unknown' },
+ labels: {
+ feature_category: 'unknown',
+ endpoint_id: 'unknown',
+ request_urgency: :default
+ },
success: false
)
subject.call(env)
@@ -303,17 +329,25 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
{ 'REQUEST_METHOD' => 'GET' }
end
- it "falls back request's expectation to medium (1 second)" do
+ it "falls back request's expectation to default (1 second)" do
allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(100, 100.9)
expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment).with(
- labels: { feature_category: 'unknown', endpoint_id: 'unknown' },
+ labels: {
+ feature_category: 'unknown',
+ endpoint_id: 'unknown',
+ request_urgency: :default
+ },
success: true
)
subject.call(env)
allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(100, 101)
expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment).with(
- labels: { feature_category: 'unknown', endpoint_id: 'unknown' },
+ labels: {
+ feature_category: 'unknown',
+ endpoint_id: 'unknown',
+ request_urgency: :default
+ },
success: false
)
subject.call(env)
diff --git a/spec/lib/gitlab/metrics/samplers/action_cable_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/action_cable_sampler_spec.rb
index f751416f4ec..d834b796179 100644
--- a/spec/lib/gitlab/metrics/samplers/action_cable_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/action_cable_sampler_spec.rb
@@ -23,64 +23,46 @@ RSpec.describe Gitlab::Metrics::Samplers::ActionCableSampler do
allow(pool).to receive(:queue_length).and_return(6)
end
- shared_examples 'collects metrics' do |expected_labels|
- it 'includes active connections' do
- expect(subject.metrics[:active_connections]).to receive(:set).with(expected_labels, 0)
+ it 'includes active connections' do
+ expect(subject.metrics[:active_connections]).to receive(:set).with({}, 0)
- subject.sample
- end
-
- it 'includes minimum worker pool size' do
- expect(subject.metrics[:pool_min_size]).to receive(:set).with(expected_labels, 1)
-
- subject.sample
- end
-
- it 'includes maximum worker pool size' do
- expect(subject.metrics[:pool_max_size]).to receive(:set).with(expected_labels, 2)
-
- subject.sample
- end
+ subject.sample
+ end
- it 'includes current worker pool size' do
- expect(subject.metrics[:pool_current_size]).to receive(:set).with(expected_labels, 3)
+ it 'includes minimum worker pool size' do
+ expect(subject.metrics[:pool_min_size]).to receive(:set).with({}, 1)
- subject.sample
- end
+ subject.sample
+ end
- it 'includes largest worker pool size' do
- expect(subject.metrics[:pool_largest_size]).to receive(:set).with(expected_labels, 4)
+ it 'includes maximum worker pool size' do
+ expect(subject.metrics[:pool_max_size]).to receive(:set).with({}, 2)
- subject.sample
- end
+ subject.sample
+ end
- it 'includes worker pool completed task count' do
- expect(subject.metrics[:pool_completed_tasks]).to receive(:set).with(expected_labels, 5)
+ it 'includes current worker pool size' do
+ expect(subject.metrics[:pool_current_size]).to receive(:set).with({}, 3)
- subject.sample
- end
+ subject.sample
+ end
- it 'includes worker pool pending task count' do
- expect(subject.metrics[:pool_pending_tasks]).to receive(:set).with(expected_labels, 6)
+ it 'includes largest worker pool size' do
+ expect(subject.metrics[:pool_largest_size]).to receive(:set).with({}, 4)
- subject.sample
- end
+ subject.sample
end
- context 'for in-app mode' do
- before do
- expect(Gitlab::ActionCable::Config).to receive(:in_app?).and_return(true)
- end
+ it 'includes worker pool completed task count' do
+ expect(subject.metrics[:pool_completed_tasks]).to receive(:set).with({}, 5)
- it_behaves_like 'collects metrics', server_mode: 'in-app'
+ subject.sample
end
- context 'for standalone mode' do
- before do
- expect(Gitlab::ActionCable::Config).to receive(:in_app?).and_return(false)
- end
+ it 'includes worker pool pending task count' do
+ expect(subject.metrics[:pool_pending_tasks]).to receive(:set).with({}, 6)
- it_behaves_like 'collects metrics', server_mode: 'standalone'
+ subject.sample
end
end
end
diff --git a/spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb
index 7dda10ab41d..e97a4fdddcb 100644
--- a/spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb
@@ -18,8 +18,8 @@ RSpec.describe Gitlab::Metrics::Samplers::DatabaseSampler do
let(:labels) do
{
class: 'ActiveRecord::Base',
- host: Gitlab::Database.main.config['host'],
- port: Gitlab::Database.main.config['port']
+ host: ApplicationRecord.database.config['host'],
+ port: ApplicationRecord.database.config['port']
}
end
diff --git a/spec/lib/gitlab/metrics/subscribers/external_http_spec.rb b/spec/lib/gitlab/metrics/subscribers/external_http_spec.rb
index adbc05cb711..e489ac97b9c 100644
--- a/spec/lib/gitlab/metrics/subscribers/external_http_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/external_http_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Metrics::Subscribers::ExternalHttp, :request_store do
- let(:transaction) { Gitlab::Metrics::Transaction.new }
+ let(:transaction) { Gitlab::Metrics::WebTransaction.new({}) }
let(:subscriber) { described_class.new }
around do |example|
diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb
index 2ff8efcd7cb..b1c15db5193 100644
--- a/spec/lib/gitlab/metrics/transaction_spec.rb
+++ b/spec/lib/gitlab/metrics/transaction_spec.rb
@@ -3,172 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Metrics::Transaction do
- let(:transaction) { described_class.new }
-
- let(:sensitive_tags) do
- {
- path: 'private',
- branch: 'sensitive'
- }
- end
-
- describe '#method_call_for' do
- it 'returns a MethodCall' do
- method = transaction.method_call_for('Foo#bar', :Foo, '#bar')
-
- expect(method).to be_an_instance_of(Gitlab::Metrics::MethodCall)
- end
- end
-
describe '#run' do
- specify { expect { transaction.run }.to raise_error(NotImplementedError) }
- end
-
- describe '#add_event' do
- let(:prometheus_metric) { instance_double(Prometheus::Client::Counter, increment: nil, base_labels: {}) }
-
- it 'adds a metric' do
- expect(prometheus_metric).to receive(:increment)
- expect(described_class).to receive(:fetch_metric).with(:counter, :gitlab_transaction_event_meow_total).and_return(prometheus_metric)
-
- transaction.add_event(:meow)
- end
-
- it 'allows tracking of custom tags' do
- expect(prometheus_metric).to receive(:increment).with(hash_including(animal: "dog"))
- expect(described_class).to receive(:fetch_metric).with(:counter, :gitlab_transaction_event_bau_total).and_return(prometheus_metric)
-
- transaction.add_event(:bau, animal: 'dog')
- end
-
- context 'with sensitive tags' do
- before do
- transaction.add_event(:baubau, **sensitive_tags.merge(sane: 'yes'))
- allow(described_class).to receive(:prometheus_metric).and_return(prometheus_metric)
- end
-
- it 'filters tags' do
- expect(prometheus_metric).not_to receive(:increment).with(hash_including(sensitive_tags))
-
- transaction.add_event(:baubau, **sensitive_tags.merge(sane: 'yes'))
- end
- end
- end
-
- describe '#increment' do
- let(:prometheus_metric) { instance_double(Prometheus::Client::Counter, increment: nil, base_labels: {}) }
-
- it 'adds a metric' do
- expect(prometheus_metric).to receive(:increment)
- expect(::Gitlab::Metrics).to receive(:counter).with(:meow, 'Meow counter', hash_including(:controller, :action)).and_return(prometheus_metric)
-
- transaction.increment(:meow, 1)
- end
-
- context 'with block' do
- it 'overrides docstring' do
- expect(::Gitlab::Metrics).to receive(:counter).with(:block_docstring, 'test', hash_including(:controller, :action)).and_return(prometheus_metric)
-
- transaction.increment(:block_docstring, 1) do
- docstring 'test'
- end
- end
-
- it 'overrides labels' do
- expect(::Gitlab::Metrics).to receive(:counter).with(:block_labels, 'Block labels counter', hash_including(:controller, :action, :sane)).and_return(prometheus_metric)
-
- labels = { sane: 'yes' }
- transaction.increment(:block_labels, 1, labels) do
- label_keys %i(sane)
- end
- end
-
- it 'filters sensitive tags' do
- expect(::Gitlab::Metrics).to receive(:counter).with(:metric_with_sensitive_block, 'Metric with sensitive block counter', hash_excluding(sensitive_tags)).and_return(prometheus_metric)
-
- labels_keys = sensitive_tags.keys
- transaction.increment(:metric_with_sensitive_block, 1, sensitive_tags) do
- label_keys labels_keys
- end
- end
- end
- end
-
- describe '#set' do
- let(:prometheus_metric) { instance_double(Prometheus::Client::Gauge, set: nil, base_labels: {}) }
-
- it 'adds a metric' do
- expect(prometheus_metric).to receive(:set)
- expect(::Gitlab::Metrics).to receive(:gauge).with(:meow_set, 'Meow set gauge', hash_including(:controller, :action), :all).and_return(prometheus_metric)
-
- transaction.set(:meow_set, 1)
- end
-
- context 'with block' do
- it 'overrides docstring' do
- expect(::Gitlab::Metrics).to receive(:gauge).with(:block_docstring_set, 'test', hash_including(:controller, :action), :all).and_return(prometheus_metric)
-
- transaction.set(:block_docstring_set, 1) do
- docstring 'test'
- end
- end
-
- it 'overrides labels' do
- expect(::Gitlab::Metrics).to receive(:gauge).with(:block_labels_set, 'Block labels set gauge', hash_including(:controller, :action, :sane), :all).and_return(prometheus_metric)
-
- labels = { sane: 'yes' }
- transaction.set(:block_labels_set, 1, labels) do
- label_keys %i(sane)
- end
- end
-
- it 'filters sensitive tags' do
- expect(::Gitlab::Metrics).to receive(:gauge).with(:metric_set_with_sensitive_block, 'Metric set with sensitive block gauge', hash_excluding(sensitive_tags), :all).and_return(prometheus_metric)
-
- label_keys = sensitive_tags.keys
- transaction.set(:metric_set_with_sensitive_block, 1, sensitive_tags) do
- label_keys label_keys
- end
- end
- end
- end
-
- describe '#observe' do
- let(:prometheus_metric) { instance_double(Prometheus::Client::Histogram, observe: nil, base_labels: {}) }
-
- it 'adds a metric' do
- expect(prometheus_metric).to receive(:observe)
- expect(::Gitlab::Metrics).to receive(:histogram).with(:meow_observe, 'Meow observe histogram', hash_including(:controller, :action), kind_of(Array)).and_return(prometheus_metric)
-
- transaction.observe(:meow_observe, 1)
- end
-
- context 'with block' do
- it 'overrides docstring' do
- expect(::Gitlab::Metrics).to receive(:histogram).with(:block_docstring_observe, 'test', hash_including(:controller, :action), kind_of(Array)).and_return(prometheus_metric)
-
- transaction.observe(:block_docstring_observe, 1) do
- docstring 'test'
- end
- end
-
- it 'overrides labels' do
- expect(::Gitlab::Metrics).to receive(:histogram).with(:block_labels_observe, 'Block labels observe histogram', hash_including(:controller, :action, :sane), kind_of(Array)).and_return(prometheus_metric)
-
- labels = { sane: 'yes' }
- transaction.observe(:block_labels_observe, 1, labels) do
- label_keys %i(sane)
- end
- end
-
- it 'filters sensitive tags' do
- expect(::Gitlab::Metrics).to receive(:histogram).with(:metric_observe_with_sensitive_block, 'Metric observe with sensitive block histogram', hash_excluding(sensitive_tags), kind_of(Array)).and_return(prometheus_metric)
-
- label_keys = sensitive_tags.keys
- transaction.observe(:metric_observe_with_sensitive_block, 1, sensitive_tags) do
- label_keys label_keys
- end
- end
- end
+ specify { expect { described_class.new.run }.to raise_error(NotImplementedError) }
end
end
diff --git a/spec/lib/gitlab/metrics/web_transaction_spec.rb b/spec/lib/gitlab/metrics/web_transaction_spec.rb
index 9e22dccb2a2..06ce58a9e84 100644
--- a/spec/lib/gitlab/metrics/web_transaction_spec.rb
+++ b/spec/lib/gitlab/metrics/web_transaction_spec.rb
@@ -5,41 +5,14 @@ require 'spec_helper'
RSpec.describe Gitlab::Metrics::WebTransaction do
let(:env) { {} }
let(:transaction) { described_class.new(env) }
- let(:prometheus_metric) { instance_double(Prometheus::Client::Metric, base_labels: {}) }
- before do
- allow(described_class).to receive(:prometheus_metric).and_return(prometheus_metric)
- end
-
- RSpec.shared_context 'ActionController request' do
- let(:request) { double(:request, format: double(:format, ref: :html)) }
- let(:controller_class) { double(:controller_class, name: 'TestController') }
-
- before do
- controller = double(:controller, class: controller_class, action_name: 'show', request: request)
- env['action_controller.instance'] = controller
- end
- end
+ describe '#run' do
+ let(:prometheus_metric) { instance_double(Prometheus::Client::Metric, base_labels: {}) }
- RSpec.shared_context 'transaction observe metrics' do
before do
+ allow(described_class).to receive(:prometheus_metric).and_return(prometheus_metric)
allow(transaction).to receive(:observe)
end
- end
-
- RSpec.shared_examples 'metric with labels' do |metric_method|
- include_context 'ActionController request'
-
- it 'measures with correct labels and value' do
- value = 1
- expect(prometheus_metric).to receive(metric_method).with({ controller: 'TestController', action: 'show', feature_category: ::Gitlab::FeatureCategories::FEATURE_CATEGORY_DEFAULT }, value)
-
- transaction.send(metric_method, :bau, value)
- end
- end
-
- describe '#run' do
- include_context 'transaction observe metrics'
it 'yields the supplied block' do
expect { |b| transaction.run(&b) }.to yield_control
@@ -88,14 +61,6 @@ RSpec.describe Gitlab::Metrics::WebTransaction do
end
end
- describe '#method_call_for' do
- it 'returns a MethodCall' do
- method = transaction.method_call_for('Foo#bar', :Foo, '#bar')
-
- expect(method).to be_an_instance_of(Gitlab::Metrics::MethodCall)
- end
- end
-
describe '#labels' do
context 'when request goes to Grape endpoint' do
before do
@@ -115,7 +80,7 @@ RSpec.describe Gitlab::Metrics::WebTransaction do
end
it 'contains only the labels defined for transactions' do
- expect(transaction.labels.keys).to contain_exactly(*described_class.superclass::BASE_LABEL_KEYS)
+ expect(transaction.labels.keys).to contain_exactly(*described_class::BASE_LABEL_KEYS)
end
it 'does not provide labels if route infos are missing' do
@@ -129,14 +94,20 @@ RSpec.describe Gitlab::Metrics::WebTransaction do
end
context 'when request goes to ActionController' do
- include_context 'ActionController request'
+ let(:request) { double(:request, format: double(:format, ref: :html)) }
+ let(:controller_class) { double(:controller_class, name: 'TestController') }
+
+ before do
+ controller = double(:controller, class: controller_class, action_name: 'show', request: request)
+ env['action_controller.instance'] = controller
+ end
it 'tags a transaction with the name and action of a controller' do
expect(transaction.labels).to eq({ controller: 'TestController', action: 'show', feature_category: ::Gitlab::FeatureCategories::FEATURE_CATEGORY_DEFAULT })
end
it 'contains only the labels defined for transactions' do
- expect(transaction.labels.keys).to contain_exactly(*described_class.superclass::BASE_LABEL_KEYS)
+ expect(transaction.labels.keys).to contain_exactly(*described_class::BASE_LABEL_KEYS)
end
context 'when the request content type is not :html' do
@@ -170,37 +141,16 @@ RSpec.describe Gitlab::Metrics::WebTransaction do
end
end
- describe '#add_event' do
- let(:prometheus_metric) { instance_double(Prometheus::Client::Counter, :increment, base_labels: {}) }
-
- it 'adds a metric' do
- expect(prometheus_metric).to receive(:increment)
-
- transaction.add_event(:meow)
- end
+ it_behaves_like 'transaction metrics with labels' do
+ let(:request) { double(:request, format: double(:format, ref: :html)) }
+ let(:controller_class) { double(:controller_class, name: 'TestController') }
+ let(:controller) { double(:controller, class: controller_class, action_name: 'show', request: request) }
- it 'allows tracking of custom tags' do
- expect(prometheus_metric).to receive(:increment).with(animal: "dog")
+ let(:transaction_obj) { described_class.new({ 'action_controller.instance' => controller }) }
+ let(:labels) { { controller: 'TestController', action: 'show', feature_category: 'projects' } }
- transaction.add_event(:bau, animal: 'dog')
+ before do
+ ::Gitlab::ApplicationContext.push(feature_category: 'projects')
end
end
-
- describe '#increment' do
- let(:prometheus_metric) { instance_double(Prometheus::Client::Counter, :increment, base_labels: {}) }
-
- it_behaves_like 'metric with labels', :increment
- end
-
- describe '#set' do
- let(:prometheus_metric) { instance_double(Prometheus::Client::Gauge, :set, base_labels: {}) }
-
- it_behaves_like 'metric with labels', :set
- end
-
- describe '#observe' do
- let(:prometheus_metric) { instance_double(Prometheus::Client::Histogram, :observe, base_labels: {}) }
-
- it_behaves_like 'metric with labels', :observe
- end
end
diff --git a/spec/lib/gitlab/middleware/compressed_json_spec.rb b/spec/lib/gitlab/middleware/compressed_json_spec.rb
new file mode 100644
index 00000000000..c5efc568971
--- /dev/null
+++ b/spec/lib/gitlab/middleware/compressed_json_spec.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Middleware::CompressedJson do
+ let_it_be(:decompressed_input) { '{"foo": "bar"}' }
+ let_it_be(:input) { ActiveSupport::Gzip.compress(decompressed_input) }
+
+ let(:app) { double(:app) }
+ let(:middleware) { described_class.new(app) }
+ let(:env) do
+ {
+ 'HTTP_CONTENT_ENCODING' => 'gzip',
+ 'REQUEST_METHOD' => 'POST',
+ 'CONTENT_TYPE' => 'application/json',
+ 'PATH_INFO' => path,
+ 'rack.input' => StringIO.new(input)
+ }
+ end
+
+ shared_examples 'decompress middleware' do
+ it 'replaces input with a decompressed content' do
+ expect(app).to receive(:call)
+
+ middleware.call(env)
+
+ expect(env['rack.input'].read).to eq(decompressed_input)
+ expect(env['CONTENT_LENGTH']).to eq(decompressed_input.length)
+ expect(env['HTTP_CONTENT_ENCODING']).to be_nil
+ end
+ end
+
+ describe '#call' do
+ context 'with collector route' do
+ let(:path) { '/api/v4/error_tracking/collector/1/store'}
+
+ it_behaves_like 'decompress middleware'
+ end
+
+ context 'with collector route under relative url' do
+ let(:path) { '/gitlab/api/v4/error_tracking/collector/1/store'}
+
+ before do
+ stub_config_setting(relative_url_root: '/gitlab')
+ end
+
+ it_behaves_like 'decompress middleware'
+ end
+
+ context 'with some other route' do
+ let(:path) { '/api/projects/123' }
+
+ it 'keeps the original input' do
+ expect(app).to receive(:call)
+
+ middleware.call(env)
+
+ expect(env['rack.input'].read).to eq(input)
+ expect(env['HTTP_CONTENT_ENCODING']).to eq('gzip')
+ end
+ end
+
+ context 'payload is too large' do
+ let(:body_limit) { Gitlab::Middleware::CompressedJson::MAXIMUM_BODY_SIZE }
+ let(:decompressed_input) { 'a' * (body_limit + 100) }
+ let(:input) { ActiveSupport::Gzip.compress(decompressed_input) }
+ let(:path) { '/api/v4/error_tracking/collector/1/envelope'}
+
+ it 'reads only limited size' do
+ expect(middleware.call(env))
+ .to eq([413, { 'Content-Type' => 'text/plain' }, ['Payload Too Large']])
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb
index 0ce95fdb5af..1ef548ab29b 100644
--- a/spec/lib/gitlab/middleware/go_spec.rb
+++ b/spec/lib/gitlab/middleware/go_spec.rb
@@ -147,6 +147,22 @@ RSpec.describe Gitlab::Middleware::Go do
end
end
end
+
+ context 'when a personal access token is missing' do
+ before do
+ env['REMOTE_ADDR'] = '192.168.0.1'
+ env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(current_user.username, 'dummy_password')
+ end
+
+ it 'returns unauthorized' do
+ expect(Gitlab::Auth).to receive(:find_for_git_client).and_raise(Gitlab::Auth::MissingPersonalAccessTokenError)
+ response = go
+
+ expect(response[0]).to eq(401)
+ expect(response[1]['Content-Length']).to be_nil
+ expect(response[2]).to eq([''])
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/middleware/query_analyzer_spec.rb b/spec/lib/gitlab/middleware/query_analyzer_spec.rb
new file mode 100644
index 00000000000..5ebe6a92da6
--- /dev/null
+++ b/spec/lib/gitlab/middleware/query_analyzer_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Middleware::QueryAnalyzer, query_analyzers: false do
+ describe 'the PreventCrossDatabaseModification' do
+ describe '#call' do
+ let(:app) { double(:app) }
+ let(:middleware) { described_class.new(app) }
+ let(:env) { {} }
+
+ subject { middleware.call(env) }
+
+ context 'when there is a cross modification' do
+ before do
+ allow(app).to receive(:call) do
+ Project.transaction do
+ Project.where(id: -1).update_all(id: -1)
+ ::Ci::Pipeline.where(id: -1).update_all(id: -1)
+ end
+ end
+ end
+
+ it 'detects cross modifications and tracks exception' do
+ expect(::Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
+
+ expect { subject }.not_to raise_error
+ end
+
+ context 'when the detect_cross_database_modification is disabled' do
+ before do
+ stub_feature_flags(detect_cross_database_modification: false)
+ end
+
+ it 'does not detect cross modifications' do
+ expect(::Gitlab::ErrorTracking).not_to receive(:track_and_raise_for_dev_exception)
+
+ subject
+ end
+ end
+ end
+
+ context 'when there is no cross modification' do
+ before do
+ allow(app).to receive(:call) do
+ Project.transaction do
+ Project.where(id: -1).update_all(id: -1)
+ Namespace.where(id: -1).update_all(id: -1)
+ end
+ end
+ end
+
+ it 'does not log anything' do
+ expect(::Gitlab::ErrorTracking).not_to receive(:track_and_raise_for_dev_exception)
+
+ subject
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb
index 2f38ed58727..f0ba0f0459d 100644
--- a/spec/lib/gitlab/path_regex_spec.rb
+++ b/spec/lib/gitlab/path_regex_spec.rb
@@ -425,6 +425,9 @@ RSpec.describe Gitlab::PathRegex do
it { is_expected.not_to match('gitlab.org/') }
it { is_expected.not_to match('/gitlab.org') }
it { is_expected.not_to match('gitlab git') }
+ it { is_expected.not_to match('gitlab?') }
+ it { is_expected.to match('gitlab.org-') }
+ it { is_expected.to match('gitlab.org_') }
end
describe '.project_path_format_regex' do
@@ -437,6 +440,14 @@ RSpec.describe Gitlab::PathRegex do
it { is_expected.not_to match('?gitlab') }
it { is_expected.not_to match('git lab') }
it { is_expected.not_to match('gitlab.git') }
+ it { is_expected.not_to match('gitlab?') }
+ it { is_expected.not_to match('gitlab git') }
+ it { is_expected.to match('gitlab.org') }
+ it { is_expected.to match('gitlab.org-') }
+ it { is_expected.to match('gitlab.org_') }
+ it { is_expected.to match('gitlab.org.') }
+ it { is_expected.not_to match('gitlab.org/') }
+ it { is_expected.not_to match('/gitlab.org') }
end
context 'repository routes' do
diff --git a/spec/lib/gitlab/project_template_spec.rb b/spec/lib/gitlab/project_template_spec.rb
index 4eb13e63b46..05417e721c7 100644
--- a/spec/lib/gitlab/project_template_spec.rb
+++ b/spec/lib/gitlab/project_template_spec.rb
@@ -10,8 +10,8 @@ RSpec.describe Gitlab::ProjectTemplate do
gomicro gatsby hugo jekyll plainhtml gitbook
hexo sse_middleman gitpod_spring_petclinic nfhugo
nfjekyll nfplainhtml nfgitbook nfhexo salesforcedx
- serverless_framework jsonnet cluster_management
- kotlin_native_linux
+ serverless_framework tencent_serverless_framework
+ jsonnet cluster_management kotlin_native_linux
]
expect(described_class.all).to be_an(Array)
diff --git a/spec/lib/gitlab/prometheus_client_spec.rb b/spec/lib/gitlab/prometheus_client_spec.rb
index 82ef4675553..89ddde4a01d 100644
--- a/spec/lib/gitlab/prometheus_client_spec.rb
+++ b/spec/lib/gitlab/prometheus_client_spec.rb
@@ -107,36 +107,14 @@ RSpec.describe Gitlab::PrometheusClient do
let(:prometheus_url) {"https://prometheus.invalid.example.com/api/v1/query?query=1"}
shared_examples 'exceptions are raised' do
- it 'raises a Gitlab::PrometheusClient::ConnectionError error when a SocketError is rescued' do
- req_stub = stub_prometheus_request_with_exception(prometheus_url, SocketError)
+ Gitlab::HTTP::HTTP_ERRORS.each do |error|
+ it "raises a Gitlab::PrometheusClient::ConnectionError when a #{error} is rescued" do
+ req_stub = stub_prometheus_request_with_exception(prometheus_url, error.new)
- expect { subject }
- .to raise_error(Gitlab::PrometheusClient::ConnectionError, "Can't connect to #{prometheus_url}")
- expect(req_stub).to have_been_requested
- end
-
- it 'raises a Gitlab::PrometheusClient::ConnectionError error when a SSLError is rescued' do
- req_stub = stub_prometheus_request_with_exception(prometheus_url, OpenSSL::SSL::SSLError)
-
- expect { subject }
- .to raise_error(Gitlab::PrometheusClient::ConnectionError, "#{prometheus_url} contains invalid SSL data")
- expect(req_stub).to have_been_requested
- end
-
- it 'raises a Gitlab::PrometheusClient::ConnectionError error when a Gitlab::HTTP::ResponseError is rescued' do
- req_stub = stub_prometheus_request_with_exception(prometheus_url, Gitlab::HTTP::ResponseError)
-
- expect { subject }
- .to raise_error(Gitlab::PrometheusClient::ConnectionError, "Network connection error")
- expect(req_stub).to have_been_requested
- end
-
- it 'raises a Gitlab::PrometheusClient::ConnectionError error when a Gitlab::HTTP::ResponseError with a code is rescued' do
- req_stub = stub_prometheus_request_with_exception(prometheus_url, Gitlab::HTTP::ResponseError.new(code: 400))
-
- expect { subject }
- .to raise_error(Gitlab::PrometheusClient::ConnectionError, "Network connection error")
- expect(req_stub).to have_been_requested
+ expect { subject }
+ .to raise_error(Gitlab::PrometheusClient::ConnectionError, kind_of(String))
+ expect(req_stub).to have_been_requested
+ end
end
end
diff --git a/spec/lib/gitlab/redis/multi_store_spec.rb b/spec/lib/gitlab/redis/multi_store_spec.rb
new file mode 100644
index 00000000000..bf1bf65bb9b
--- /dev/null
+++ b/spec/lib/gitlab/redis/multi_store_spec.rb
@@ -0,0 +1,474 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Redis::MultiStore do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:redis_store_class) do
+ Class.new(Gitlab::Redis::Wrapper) do
+ def config_file_name
+ config_file_name = "spec/fixtures/config/redis_new_format_host.yml"
+ Rails.root.join(config_file_name).to_s
+ end
+
+ def self.name
+ 'Sessions'
+ end
+ end
+ end
+
+ let_it_be(:primary_db) { 1 }
+ let_it_be(:secondary_db) { 2 }
+ let_it_be(:primary_store) { create_redis_store(redis_store_class.params, db: primary_db, serializer: nil) }
+ let_it_be(:secondary_store) { create_redis_store(redis_store_class.params, db: secondary_db, serializer: nil) }
+ let_it_be(:instance_name) { 'TestStore' }
+ let_it_be(:multi_store) { described_class.new(primary_store, secondary_store, instance_name)}
+
+ subject { multi_store.send(name, *args) }
+
+ after(:all) do
+ primary_store.flushdb
+ secondary_store.flushdb
+ end
+
+ context 'when primary_store is nil' do
+ let(:multi_store) { described_class.new(nil, secondary_store, instance_name)}
+
+ it 'fails with exception' do
+ expect { multi_store }.to raise_error(ArgumentError, /primary_store is required/)
+ end
+ end
+
+ context 'when secondary_store is nil' do
+ let(:multi_store) { described_class.new(primary_store, nil, instance_name)}
+
+ it 'fails with exception' do
+ expect { multi_store }.to raise_error(ArgumentError, /secondary_store is required/)
+ end
+ end
+
+ context 'when primary_store is not a ::Redis instance' do
+ before do
+ allow(primary_store).to receive(:is_a?).with(::Redis).and_return(false)
+ end
+
+ it 'fails with exception' do
+ expect { described_class.new(primary_store, secondary_store, instance_name) }.to raise_error(ArgumentError, /invalid primary_store/)
+ end
+ end
+
+ context 'when secondary_store is not a ::Redis instance' do
+ before do
+ allow(secondary_store).to receive(:is_a?).with(::Redis).and_return(false)
+ end
+
+ it 'fails with exception' do
+ expect { described_class.new(primary_store, secondary_store, instance_name) }.to raise_error(ArgumentError, /invalid secondary_store/)
+ end
+ end
+
+ context 'with READ redis commands' do
+ let_it_be(:key1) { "redis:{1}:key_a" }
+ let_it_be(:key2) { "redis:{1}:key_b" }
+ let_it_be(:value1) { "redis_value1"}
+ let_it_be(:value2) { "redis_value2"}
+ let_it_be(:skey) { "redis:set:key" }
+ let_it_be(:keys) { [key1, key2] }
+ let_it_be(:values) { [value1, value2] }
+ let_it_be(:svalues) { [value2, value1] }
+
+ where(:case_name, :name, :args, :value, :block) do
+ 'execute :get command' | :get | ref(:key1) | ref(:value1) | nil
+ 'execute :mget command' | :mget | ref(:keys) | ref(:values) | nil
+ 'execute :mget with block' | :mget | ref(:keys) | ref(:values) | ->(value) { value }
+ 'execute :smembers command' | :smembers | ref(:skey) | ref(:svalues) | nil
+ 'execute :scard command' | :scard | ref(:skey) | 2 | nil
+ end
+
+ before(:all) do
+ primary_store.multi do |multi|
+ multi.set(key1, value1)
+ multi.set(key2, value2)
+ multi.sadd(skey, value1)
+ multi.sadd(skey, value2)
+ end
+
+ secondary_store.multi do |multi|
+ multi.set(key1, value1)
+ multi.set(key2, value2)
+ multi.sadd(skey, value1)
+ multi.sadd(skey, value2)
+ end
+ end
+
+ RSpec.shared_examples_for 'reads correct value' do
+ it 'returns the correct value' do
+ if value.is_a?(Array)
+ # :smembers does not guarantee the order it will return the values (unsorted set)
+ is_expected.to match_array(value)
+ else
+ is_expected.to eq(value)
+ end
+ end
+ end
+
+ RSpec.shared_examples_for 'fallback read from the secondary store' do
+ it 'fallback and execute on secondary instance' do
+ expect(secondary_store).to receive(name).with(*args).and_call_original
+
+ subject
+ end
+
+ it 'logs the ReadFromPrimaryError' do
+ expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(Gitlab::Redis::MultiStore::ReadFromPrimaryError),
+ hash_including(command_name: name, extra: hash_including(instance_name: instance_name)))
+
+ subject
+ end
+
+ it 'increment read fallback count metrics' do
+ expect(multi_store).to receive(:increment_read_fallback_count).with(name)
+
+ subject
+ end
+
+ include_examples 'reads correct value'
+
+ context 'when fallback read from the secondary instance raises an exception' do
+ before do
+ allow(secondary_store).to receive(name).with(*args).and_raise(StandardError)
+ end
+
+ it 'fails with exception' do
+ expect { subject }.to raise_error(StandardError)
+ end
+ end
+ end
+
+ RSpec.shared_examples_for 'secondary store' do
+ it 'execute on the secondary instance' do
+ expect(secondary_store).to receive(name).with(*args).and_call_original
+
+ subject
+ end
+
+ include_examples 'reads correct value'
+
+ it 'does not execute on the primary store' do
+ expect(primary_store).not_to receive(name)
+
+ subject
+ end
+ end
+
+ with_them do
+ describe "#{name}" do
+ before do
+ allow(primary_store).to receive(name).and_call_original
+ allow(secondary_store).to receive(name).and_call_original
+ end
+
+ context 'with feature flag :use_multi_store enabled' do
+ before do
+ stub_feature_flags(use_multi_store: true)
+ end
+
+ context 'when reading from the primary is successful' do
+ it 'returns the correct value' do
+ expect(primary_store).to receive(name).with(*args).and_call_original
+
+ subject
+ end
+
+ it 'does not execute on the secondary store' do
+ expect(secondary_store).not_to receive(name)
+
+ subject
+ end
+
+ include_examples 'reads correct value'
+ end
+
+ context 'when reading from primary instance is raising an exception' do
+ before do
+ allow(primary_store).to receive(name).with(*args).and_raise(StandardError)
+ allow(Gitlab::ErrorTracking).to receive(:log_exception)
+ end
+
+ it 'logs the exception' do
+ expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(StandardError),
+ hash_including(extra: hash_including(:multi_store_error_message, instance_name: instance_name),
+ command_name: name))
+
+ subject
+ end
+
+ include_examples 'fallback read from the secondary store'
+ end
+
+ context 'when reading from primary instance return no value' do
+ before do
+ allow(primary_store).to receive(name).and_return(nil)
+ end
+
+ include_examples 'fallback read from the secondary store'
+ end
+
+ context 'when the command is executed within pipelined block' do
+ subject do
+ multi_store.pipelined do
+ multi_store.send(name, *args)
+ end
+ end
+
+ it 'is executed only 1 time on primary instance' do
+ expect(primary_store).to receive(name).with(*args).once
+
+ subject
+ end
+ end
+
+ if params[:block]
+ subject do
+ multi_store.send(name, *args, &block)
+ end
+
+ context 'when block is provided' do
+ it 'yields to the block' do
+ expect(primary_store).to receive(name).and_yield(value)
+
+ subject
+ end
+
+ include_examples 'reads correct value'
+ end
+ end
+ end
+
+ context 'with feature flag :use_multi_store is disabled' do
+ before do
+ stub_feature_flags(use_multi_store: false)
+ end
+
+ it_behaves_like 'secondary store'
+ end
+
+ context 'with both primary and secondary store using same redis instance' do
+ let(:primary_store) { create_redis_store(redis_store_class.params, db: primary_db, serializer: nil) }
+ let(:secondary_store) { create_redis_store(redis_store_class.params, db: primary_db, serializer: nil) }
+ let(:multi_store) { described_class.new(primary_store, secondary_store, instance_name)}
+
+ it_behaves_like 'secondary store'
+ end
+ end
+ end
+ end
+
+ context 'with WRITE redis commands' do
+ let_it_be(:key1) { "redis:{1}:key_a" }
+ let_it_be(:key2) { "redis:{1}:key_b" }
+ let_it_be(:value1) { "redis_value1"}
+ let_it_be(:value2) { "redis_value2"}
+ let_it_be(:key1_value1) { [key1, value1] }
+ let_it_be(:key1_value2) { [key1, value2] }
+ let_it_be(:ttl) { 10 }
+ let_it_be(:key1_ttl_value1) { [key1, ttl, value1] }
+ let_it_be(:skey) { "redis:set:key" }
+ let_it_be(:svalues1) { [value2, value1] }
+ let_it_be(:svalues2) { [value1] }
+ let_it_be(:skey_value1) { [skey, value1] }
+ let_it_be(:skey_value2) { [skey, value2] }
+
+ where(:case_name, :name, :args, :expected_value, :verification_name, :verification_args) do
+ 'execute :set command' | :set | ref(:key1_value1) | ref(:value1) | :get | ref(:key1)
+ 'execute :setnx command' | :setnx | ref(:key1_value2) | ref(:value1) | :get | ref(:key2)
+ 'execute :setex command' | :setex | ref(:key1_ttl_value1) | ref(:ttl) | :ttl | ref(:key1)
+ 'execute :sadd command' | :sadd | ref(:skey_value2) | ref(:svalues1) | :smembers | ref(:skey)
+ 'execute :srem command' | :srem | ref(:skey_value1) | [] | :smembers | ref(:skey)
+ 'execute :del command' | :del | ref(:key2) | nil | :get | ref(:key2)
+ 'execute :flushdb command' | :flushdb | nil | 0 | :dbsize | nil
+ end
+
+ before do
+ primary_store.flushdb
+ secondary_store.flushdb
+
+ primary_store.multi do |multi|
+ multi.set(key2, value1)
+ multi.sadd(skey, value1)
+ end
+
+ secondary_store.multi do |multi|
+ multi.set(key2, value1)
+ multi.sadd(skey, value1)
+ end
+ end
+
+ RSpec.shared_examples_for 'verify that store contains values' do |store|
+ it "#{store} redis store contains correct values", :aggregate_errors do
+ subject
+
+ redis_store = multi_store.send(store)
+
+ if expected_value.is_a?(Array)
+ # :smembers does not guarantee the order it will return the values
+ expect(redis_store.send(verification_name, *verification_args)).to match_array(expected_value)
+ else
+ expect(redis_store.send(verification_name, *verification_args)).to eq(expected_value)
+ end
+ end
+ end
+
+ with_them do
+ describe "#{name}" do
+ let(:expected_args) {args || no_args }
+
+ before do
+ allow(primary_store).to receive(name).and_call_original
+ allow(secondary_store).to receive(name).and_call_original
+ end
+
+ context 'with feature flag :use_multi_store enabled' do
+ before do
+ stub_feature_flags(use_multi_store: true)
+ end
+
+ context 'when executing on primary instance is successful' do
+ it 'executes on both primary and secondary redis store', :aggregate_errors do
+ expect(primary_store).to receive(name).with(*expected_args).and_call_original
+ expect(secondary_store).to receive(name).with(*expected_args).and_call_original
+
+ subject
+ end
+
+ include_examples 'verify that store contains values', :primary_store
+ include_examples 'verify that store contains values', :secondary_store
+ end
+
+ context 'when executing on the primary instance is raising an exception' do
+ before do
+ allow(primary_store).to receive(name).with(*expected_args).and_raise(StandardError)
+ allow(Gitlab::ErrorTracking).to receive(:log_exception)
+ end
+
+ it 'logs the exception and execute on secondary instance', :aggregate_errors do
+ expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(StandardError),
+ hash_including(extra: hash_including(:multi_store_error_message), command_name: name))
+ expect(secondary_store).to receive(name).with(*expected_args).and_call_original
+
+ subject
+ end
+
+ include_examples 'verify that store contains values', :secondary_store
+ end
+
+ context 'when the command is executed within pipelined block' do
+ subject do
+ multi_store.pipelined do
+ multi_store.send(name, *args)
+ end
+ end
+
+ it 'is executed only 1 time on each instance', :aggregate_errors do
+ expect(primary_store).to receive(name).with(*expected_args).once
+ expect(secondary_store).to receive(name).with(*expected_args).once
+
+ subject
+ end
+
+ include_examples 'verify that store contains values', :primary_store
+ include_examples 'verify that store contains values', :secondary_store
+ end
+ end
+
+ context 'with feature flag :use_multi_store is disabled' do
+ before do
+ stub_feature_flags(use_multi_store: false)
+ end
+
+ it 'executes only on the secondary redis store', :aggregate_errors do
+ expect(secondary_store).to receive(name).with(*expected_args)
+ expect(primary_store).not_to receive(name).with(*expected_args)
+
+ subject
+ end
+
+ include_examples 'verify that store contains values', :secondary_store
+ end
+ end
+ end
+ end
+
+ context 'with unsupported command' do
+ before do
+ primary_store.flushdb
+ secondary_store.flushdb
+ end
+
+ let_it_be(:key) { "redis:counter" }
+
+ subject do
+ multi_store.incr(key)
+ end
+
+ it 'executes method missing' do
+ expect(multi_store).to receive(:method_missing)
+
+ subject
+ end
+
+ it 'logs MethodMissingError' do
+ expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(Gitlab::Redis::MultiStore::MethodMissingError),
+ hash_including(command_name: :incr, extra: hash_including(instance_name: instance_name)))
+
+ subject
+ end
+
+ it 'increments method missing counter' do
+ expect(multi_store).to receive(:increment_method_missing_count).with(:incr)
+
+ subject
+ end
+
+ it 'fallback and executes only on the secondary store', :aggregate_errors do
+ expect(secondary_store).to receive(:incr).with(key).and_call_original
+ expect(primary_store).not_to receive(:incr)
+
+ subject
+ end
+
+ it 'correct value is stored on the secondary store', :aggregate_errors do
+ subject
+
+ expect(primary_store.get(key)).to be_nil
+ expect(secondary_store.get(key)).to eq('1')
+ end
+
+ context 'when the command is executed within pipelined block' do
+ subject do
+ multi_store.pipelined do
+ multi_store.incr(key)
+ end
+ end
+
+ it 'is executed only 1 time on each instance', :aggregate_errors do
+ expect(primary_store).to receive(:incr).with(key).once
+ expect(secondary_store).to receive(:incr).with(key).once
+
+ subject
+ end
+
+ it "both redis stores are containing correct values", :aggregate_errors do
+ subject
+
+ expect(primary_store.get(key)).to eq('1')
+ expect(secondary_store.get(key)).to eq('1')
+ end
+ end
+ end
+
+ def create_redis_store(options, extras = {})
+ ::Redis::Store.new(options.merge(extras))
+ end
+end
diff --git a/spec/lib/gitlab/runtime_spec.rb b/spec/lib/gitlab/runtime_spec.rb
index f51c5dd3d20..4627a8db82e 100644
--- a/spec/lib/gitlab/runtime_spec.rb
+++ b/spec/lib/gitlab/runtime_spec.rb
@@ -48,10 +48,9 @@ RSpec.describe Gitlab::Runtime do
before do
stub_const('::Puma', puma_type)
- stub_env('ACTION_CABLE_IN_APP', 'false')
end
- it_behaves_like "valid runtime", :puma, 1
+ it_behaves_like "valid runtime", :puma, 1 + Gitlab::ActionCable::Config.worker_pool_size
end
context "puma with cli_config" do
@@ -61,27 +60,16 @@ RSpec.describe Gitlab::Runtime do
before do
stub_const('::Puma', puma_type)
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
- it_behaves_like "valid runtime", :puma, 3
+ it_behaves_like "valid runtime", :puma, 3 + Gitlab::ActionCable::Config.worker_pool_size
- context "when ActionCable in-app mode is enabled" do
+ context "when ActionCable worker pool size is configured" do
before do
- stub_env('ACTION_CABLE_IN_APP', 'true')
- stub_env('ACTION_CABLE_WORKER_POOL_SIZE', '3')
+ stub_env('ACTION_CABLE_WORKER_POOL_SIZE', 10)
end
- it_behaves_like "valid runtime", :puma, 6
- end
-
- context "when ActionCable standalone is run" do
- before do
- stub_const('ACTION_CABLE_SERVER', true)
- stub_env('ACTION_CABLE_WORKER_POOL_SIZE', '8')
- end
-
- it_behaves_like "valid runtime", :puma, 11
+ it_behaves_like "valid runtime", :puma, 13
end
describe ".puma_in_clustered_mode?" do
@@ -108,7 +96,7 @@ RSpec.describe Gitlab::Runtime do
allow(sidekiq_type).to receive(:options).and_return(concurrency: 2)
end
- it_behaves_like "valid runtime", :sidekiq, 4
+ it_behaves_like "valid runtime", :sidekiq, 5
end
context "console" do
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index 27d65e14347..a38073e7c51 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -96,6 +96,18 @@ RSpec.describe Gitlab::SearchResults do
end
end
+ describe '#aggregations' do
+ where(:scope) do
+ %w(projects issues merge_requests blobs commits wiki_blobs epics milestones users unknown)
+ end
+
+ with_them do
+ it 'returns an empty array' do
+ expect(results.aggregations(scope)).to match_array([])
+ end
+ end
+ end
+
context "when count_limit is lower than total amount" do
before do
allow(results).to receive(:count_limit).and_return(1)
diff --git a/spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb b/spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb
index bc63289a344..576b36c1829 100644
--- a/spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb
+++ b/spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb
@@ -11,12 +11,12 @@ RSpec.describe Gitlab::SidekiqConfig::CliMethods do
end
def stub_exists(exists: true)
- ['app/workers/all_queues.yml', 'ee/app/workers/all_queues.yml'].each do |path|
+ ['app/workers/all_queues.yml', 'ee/app/workers/all_queues.yml', 'jh/app/workers/all_queues.yml'].each do |path|
allow(File).to receive(:exist?).with(expand_path(path)).and_return(exists)
end
end
- def stub_contents(foss_queues, ee_queues)
+ def stub_contents(foss_queues, ee_queues, jh_queues)
allow(YAML).to receive(:load_file)
.with(expand_path('app/workers/all_queues.yml'))
.and_return(foss_queues)
@@ -24,6 +24,10 @@ RSpec.describe Gitlab::SidekiqConfig::CliMethods do
allow(YAML).to receive(:load_file)
.with(expand_path('ee/app/workers/all_queues.yml'))
.and_return(ee_queues)
+
+ allow(YAML).to receive(:load_file)
+ .with(expand_path('jh/app/workers/all_queues.yml'))
+ .and_return(jh_queues)
end
before do
@@ -45,8 +49,9 @@ RSpec.describe Gitlab::SidekiqConfig::CliMethods do
end
it 'flattens and joins the contents' do
- expected_queues = %w[queue_a queue_b]
- expected_queues = expected_queues.first(1) unless Gitlab.ee?
+ expected_queues = %w[queue_a]
+ expected_queues << 'queue_b' if Gitlab.ee?
+ expected_queues << 'queue_c' if Gitlab.jh?
expect(described_class.worker_queues(dummy_root))
.to match_array(expected_queues)
@@ -55,7 +60,7 @@ RSpec.describe Gitlab::SidekiqConfig::CliMethods do
context 'when the file contains an array of hashes' do
before do
- stub_contents([{ name: 'queue_a' }], [{ name: 'queue_b' }])
+ stub_contents([{ name: 'queue_a' }], [{ name: 'queue_b' }], [{ name: 'queue_c' }])
end
include_examples 'valid file contents'
diff --git a/spec/lib/gitlab/sidekiq_config/worker_spec.rb b/spec/lib/gitlab/sidekiq_config/worker_spec.rb
index f4d7a4b3359..9c252b3d50b 100644
--- a/spec/lib/gitlab/sidekiq_config/worker_spec.rb
+++ b/spec/lib/gitlab/sidekiq_config/worker_spec.rb
@@ -18,19 +18,26 @@ RSpec.describe Gitlab::SidekiqConfig::Worker do
get_tags: attributes[:tags]
)
- described_class.new(inner_worker, ee: false)
+ described_class.new(inner_worker, ee: false, jh: false)
end
describe '#ee?' do
it 'returns the EE status set on creation' do
- expect(described_class.new(double, ee: true)).to be_ee
- expect(described_class.new(double, ee: false)).not_to be_ee
+ expect(described_class.new(double, ee: true, jh: false)).to be_ee
+ expect(described_class.new(double, ee: false, jh: false)).not_to be_ee
+ end
+ end
+
+ describe '#jh?' do
+ it 'returns the JH status set on creation' do
+ expect(described_class.new(double, ee: false, jh: true)).to be_jh
+ expect(described_class.new(double, ee: false, jh: false)).not_to be_jh
end
end
describe '#==' do
def worker_with_yaml(yaml)
- described_class.new(double, ee: false).tap do |worker|
+ described_class.new(double, ee: false, jh: false).tap do |worker|
allow(worker).to receive(:to_yaml).and_return(yaml)
end
end
@@ -57,7 +64,7 @@ RSpec.describe Gitlab::SidekiqConfig::Worker do
expect(worker).to receive(meth)
- described_class.new(worker, ee: false).send(meth)
+ described_class.new(worker, ee: false, jh: false).send(meth)
end
end
end
diff --git a/spec/lib/gitlab/sidekiq_enq_spec.rb b/spec/lib/gitlab/sidekiq_enq_spec.rb
new file mode 100644
index 00000000000..6903f01bf5f
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_enq_spec.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::SidekiqEnq, :clean_gitlab_redis_queues do
+ let(:retry_set) { Sidekiq::Scheduled::SETS.first }
+ let(:schedule_set) { Sidekiq::Scheduled::SETS.last }
+
+ around do |example|
+ freeze_time { example.run }
+ end
+
+ shared_examples 'finds jobs that are due and enqueues them' do
+ before do
+ Sidekiq.redis do |redis|
+ redis.zadd(retry_set, (Time.current - 1.day).to_f.to_s, '{"jid": 1}')
+ redis.zadd(retry_set, Time.current.to_f.to_s, '{"jid": 2}')
+ redis.zadd(retry_set, (Time.current + 1.day).to_f.to_s, '{"jid": 3}')
+
+ redis.zadd(schedule_set, (Time.current - 1.day).to_f.to_s, '{"jid": 4}')
+ redis.zadd(schedule_set, Time.current.to_f.to_s, '{"jid": 5}')
+ redis.zadd(schedule_set, (Time.current + 1.day).to_f.to_s, '{"jid": 6}')
+ end
+ end
+
+ it 'enqueues jobs that are due' do
+ expect(Sidekiq::Client).to receive(:push).with({ 'jid' => 1 })
+ expect(Sidekiq::Client).to receive(:push).with({ 'jid' => 2 })
+ expect(Sidekiq::Client).to receive(:push).with({ 'jid' => 4 })
+ expect(Sidekiq::Client).to receive(:push).with({ 'jid' => 5 })
+
+ Gitlab::SidekiqEnq.new.enqueue_jobs
+
+ Sidekiq.redis do |redis|
+ expect(redis.zscan_each(retry_set).map(&:first)).to contain_exactly('{"jid": 3}')
+ expect(redis.zscan_each(schedule_set).map(&:first)).to contain_exactly('{"jid": 6}')
+ end
+ end
+ end
+
+ context 'when atomic_sidekiq_scheduler is disabled' do
+ before do
+ stub_feature_flags(atomic_sidekiq_scheduler: false)
+ end
+
+ it_behaves_like 'finds jobs that are due and enqueues them'
+
+ context 'when ZRANGEBYSCORE returns a job that is already removed by another process' do
+ before do
+ Sidekiq.redis do |redis|
+ redis.zadd(schedule_set, Time.current.to_f.to_s, '{"jid": 1}')
+
+ allow(redis).to receive(:zrangebyscore).and_wrap_original do |m, *args, **kwargs|
+ m.call(*args, **kwargs).tap do |jobs|
+ redis.zrem(schedule_set, jobs.first) if args[0] == schedule_set && jobs.first
+ end
+ end
+ end
+ end
+
+ it 'calls ZREM but does not enqueue the job' do
+ Sidekiq.redis do |redis|
+ expect(redis).to receive(:zrem).with(schedule_set, '{"jid": 1}').twice.and_call_original
+ end
+ expect(Sidekiq::Client).not_to receive(:push)
+
+ Gitlab::SidekiqEnq.new.enqueue_jobs
+ end
+ end
+ end
+
+ context 'when atomic_sidekiq_scheduler is enabled' do
+ before do
+ stub_feature_flags(atomic_sidekiq_scheduler: true)
+ end
+
+ context 'when Lua script is not yet loaded' do
+ before do
+ Gitlab::Redis::Queues.with { |redis| redis.script(:flush) }
+ end
+
+ it_behaves_like 'finds jobs that are due and enqueues them'
+ end
+
+ context 'when Lua script is already loaded' do
+ before do
+ Gitlab::SidekiqEnq.new.enqueue_jobs
+ end
+
+ it_behaves_like 'finds jobs that are due and enqueues them'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sidekiq_logging/deduplication_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/deduplication_logger_spec.rb
index 82f927fe481..f44a1e8b6ba 100644
--- a/spec/lib/gitlab/sidekiq_logging/deduplication_logger_spec.rb
+++ b/spec/lib/gitlab/sidekiq_logging/deduplication_logger_spec.rb
@@ -23,11 +23,37 @@ RSpec.describe Gitlab::SidekiqLogging::DeduplicationLogger do
}
expect(Sidekiq.logger).to receive(:info).with(a_hash_including(expected_payload)).and_call_original
- described_class.instance.log(job, "a fancy strategy", { foo: :bar })
+ described_class.instance.deduplicated_log(job, "a fancy strategy", { foo: :bar })
end
it "does not modify the job" do
- expect { described_class.instance.log(job, "a fancy strategy") }
+ expect { described_class.instance.deduplicated_log(job, "a fancy strategy") }
+ .not_to change { job }
+ end
+ end
+
+ describe '#rescheduled_log' do
+ let(:job) do
+ {
+ 'class' => 'TestWorker',
+ 'args' => [1234, 'hello', { 'key' => 'value' }],
+ 'jid' => 'da883554ee4fe414012f5f42',
+ 'correlation_id' => 'cid'
+ }
+ end
+
+ it 'logs a rescheduled message to the sidekiq logger' do
+ expected_payload = {
+ 'job_status' => 'rescheduled',
+ 'message' => "#{job['class']} JID-#{job['jid']}: rescheduled"
+ }
+ expect(Sidekiq.logger).to receive(:info).with(a_hash_including(expected_payload)).and_call_original
+
+ described_class.instance.rescheduled_log(job)
+ end
+
+ it 'does not modify the job' do
+ expect { described_class.instance.rescheduled_log(job) }
.not_to change { job }
end
end
diff --git a/spec/lib/gitlab/sidekiq_logging/json_formatter_spec.rb b/spec/lib/gitlab/sidekiq_logging/json_formatter_spec.rb
index c879fdea3ad..b6fb3fecf20 100644
--- a/spec/lib/gitlab/sidekiq_logging/json_formatter_spec.rb
+++ b/spec/lib/gitlab/sidekiq_logging/json_formatter_spec.rb
@@ -17,6 +17,7 @@ RSpec.describe Gitlab::SidekiqLogging::JSONFormatter do
'class' => 'PostReceive',
'bar' => 'test',
'created_at' => timestamp,
+ 'scheduled_at' => timestamp,
'enqueued_at' => timestamp,
'started_at' => timestamp,
'retried_at' => timestamp,
@@ -31,6 +32,7 @@ RSpec.describe Gitlab::SidekiqLogging::JSONFormatter do
'severity' => 'INFO',
'time' => timestamp_iso8601,
'created_at' => timestamp_iso8601,
+ 'scheduled_at' => timestamp_iso8601,
'enqueued_at' => timestamp_iso8601,
'started_at' => timestamp_iso8601,
'retried_at' => timestamp_iso8601,
diff --git a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
index 5083ac514db..833de6ae624 100644
--- a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
@@ -24,6 +24,10 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
"#{Gitlab::Redis::Queues::SIDEKIQ_NAMESPACE}:duplicate:#{queue}:#{hash}"
end
+ let(:deduplicated_flag_key) do
+ "#{idempotency_key}:deduplicate_flag"
+ end
+
describe '#schedule' do
shared_examples 'scheduling with deduplication class' do |strategy_class|
it 'calls schedule on the strategy' do
@@ -81,25 +85,43 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
context 'when there was no job in the queue yet' do
it { expect(duplicate_job.check!).to eq('123') }
- it "adds a idempotency key with ttl set to #{described_class::DUPLICATE_KEY_TTL}" do
- expect { duplicate_job.check! }
- .to change { read_idempotency_key_with_ttl(idempotency_key) }
- .from([nil, -2])
- .to(['123', be_within(1).of(described_class::DUPLICATE_KEY_TTL)])
- end
-
- context 'when wal locations is not empty' do
- it "adds a existing wal locations key with ttl set to #{described_class::DUPLICATE_KEY_TTL}" do
+ shared_examples 'sets Redis keys with correct TTL' do
+ it "adds an idempotency key with correct ttl" do
expect { duplicate_job.check! }
- .to change { read_idempotency_key_with_ttl(existing_wal_location_key(idempotency_key, :main)) }
- .from([nil, -2])
- .to([wal_locations[:main], be_within(1).of(described_class::DUPLICATE_KEY_TTL)])
- .and change { read_idempotency_key_with_ttl(existing_wal_location_key(idempotency_key, :ci)) }
+ .to change { read_idempotency_key_with_ttl(idempotency_key) }
.from([nil, -2])
- .to([wal_locations[:ci], be_within(1).of(described_class::DUPLICATE_KEY_TTL)])
+ .to(['123', be_within(1).of(expected_ttl)])
+ end
+
+ context 'when wal locations is not empty' do
+ it "adds an existing wal locations key with correct ttl" do
+ expect { duplicate_job.check! }
+ .to change { read_idempotency_key_with_ttl(existing_wal_location_key(idempotency_key, :main)) }
+ .from([nil, -2])
+ .to([wal_locations[:main], be_within(1).of(expected_ttl)])
+ .and change { read_idempotency_key_with_ttl(existing_wal_location_key(idempotency_key, :ci)) }
+ .from([nil, -2])
+ .to([wal_locations[:ci], be_within(1).of(expected_ttl)])
+ end
end
end
+ context 'with TTL option is not set' do
+ let(:expected_ttl) { described_class::DEFAULT_DUPLICATE_KEY_TTL }
+
+ it_behaves_like 'sets Redis keys with correct TTL'
+ end
+
+ context 'when TTL option is set' do
+ let(:expected_ttl) { 5.minutes }
+
+ before do
+ allow(duplicate_job).to receive(:options).and_return({ ttl: expected_ttl })
+ end
+
+ it_behaves_like 'sets Redis keys with correct TTL'
+ end
+
context 'when preserve_latest_wal_locations_for_idempotent_jobs feature flag is disabled' do
before do
stub_feature_flags(preserve_latest_wal_locations_for_idempotent_jobs: false)
@@ -152,26 +174,21 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
end
describe '#update_latest_wal_location!' do
- let(:offset) { '1024' }
-
before do
- allow(duplicate_job).to receive(:pg_wal_lsn_diff).with(:main).and_return(offset)
- allow(duplicate_job).to receive(:pg_wal_lsn_diff).with(:ci).and_return(offset)
- end
+ allow(Gitlab::Database).to receive(:database_base_models).and_return(
+ { main: ::ActiveRecord::Base,
+ ci: ::ActiveRecord::Base })
- shared_examples 'updates wal location' do
- it 'updates a wal location to redis with an offset' do
- expect { duplicate_job.update_latest_wal_location! }
- .to change { read_range_from_redis(wal_location_key(idempotency_key, :main)) }
- .from(existing_wal_with_offset[:main])
- .to(new_wal_with_offset[:main])
- .and change { read_range_from_redis(wal_location_key(idempotency_key, :ci)) }
- .from(existing_wal_with_offset[:ci])
- .to(new_wal_with_offset[:ci])
- end
+ set_idempotency_key(existing_wal_location_key(idempotency_key, :main), existing_wal[:main])
+ set_idempotency_key(existing_wal_location_key(idempotency_key, :ci), existing_wal[:ci])
+
+ # read existing_wal_locations
+ duplicate_job.check!
end
context 'when preserve_latest_wal_locations_for_idempotent_jobs feature flag is disabled' do
+ let(:existing_wal) { {} }
+
before do
stub_feature_flags(preserve_latest_wal_locations_for_idempotent_jobs: false)
end
@@ -192,42 +209,107 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
end
context "when the key doesn't exists in redis" do
- include_examples 'updates wal location' do
- let(:existing_wal_with_offset) { { main: [], ci: [] } }
- let(:new_wal_with_offset) { wal_locations.transform_values { |v| [v, offset] } }
+ let(:existing_wal) do
+ {
+ main: '0/D525E3A0',
+ ci: 'AB/12340'
+ }
end
- end
- context "when the key exists in redis" do
- let(:existing_offset) { '1023'}
- let(:existing_wal_locations) do
+ let(:new_wal_location_with_offset) do
{
- main: '0/D525E3NM',
- ci: 'AB/111112'
+ # offset is relative to `existing_wal`
+ main: ['0/D525E3A8', '8'],
+ ci: ['AB/12345', '5']
}
end
+ let(:wal_locations) { new_wal_location_with_offset.transform_values(&:first) }
+
+ it 'stores a wal location to redis with an offset relative to existing wal location' do
+ expect { duplicate_job.update_latest_wal_location! }
+ .to change { read_range_from_redis(wal_location_key(idempotency_key, :main)) }
+ .from([])
+ .to(new_wal_location_with_offset[:main])
+ .and change { read_range_from_redis(wal_location_key(idempotency_key, :ci)) }
+ .from([])
+ .to(new_wal_location_with_offset[:ci])
+ end
+ end
+
+ context "when the key exists in redis" do
before do
- rpush_to_redis_key(wal_location_key(idempotency_key, :main), existing_wal_locations[:main], existing_offset)
- rpush_to_redis_key(wal_location_key(idempotency_key, :ci), existing_wal_locations[:ci], existing_offset)
+ rpush_to_redis_key(wal_location_key(idempotency_key, :main), *stored_wal_location_with_offset[:main])
+ rpush_to_redis_key(wal_location_key(idempotency_key, :ci), *stored_wal_location_with_offset[:ci])
end
+ let(:wal_locations) { new_wal_location_with_offset.transform_values(&:first) }
+
context "when the new offset is bigger then the existing one" do
- include_examples 'updates wal location' do
- let(:existing_wal_with_offset) { existing_wal_locations.transform_values { |v| [v, existing_offset] } }
- let(:new_wal_with_offset) { wal_locations.transform_values { |v| [v, offset] } }
+ let(:existing_wal) do
+ {
+ main: '0/D525E3A0',
+ ci: 'AB/12340'
+ }
+ end
+
+ let(:stored_wal_location_with_offset) do
+ {
+ # offset is relative to `existing_wal`
+ main: ['0/D525E3A3', '3'],
+ ci: ['AB/12342', '2']
+ }
+ end
+
+ let(:new_wal_location_with_offset) do
+ {
+ # offset is relative to `existing_wal`
+ main: ['0/D525E3A8', '8'],
+ ci: ['AB/12345', '5']
+ }
+ end
+
+ it 'updates a wal location to redis with an offset' do
+ expect { duplicate_job.update_latest_wal_location! }
+ .to change { read_range_from_redis(wal_location_key(idempotency_key, :main)) }
+ .from(stored_wal_location_with_offset[:main])
+ .to(new_wal_location_with_offset[:main])
+ .and change { read_range_from_redis(wal_location_key(idempotency_key, :ci)) }
+ .from(stored_wal_location_with_offset[:ci])
+ .to(new_wal_location_with_offset[:ci])
end
end
context "when the old offset is not bigger then the existing one" do
- let(:existing_offset) { offset }
+ let(:existing_wal) do
+ {
+ main: '0/D525E3A0',
+ ci: 'AB/12340'
+ }
+ end
+
+ let(:stored_wal_location_with_offset) do
+ {
+ # offset is relative to `existing_wal`
+ main: ['0/D525E3A8', '8'],
+ ci: ['AB/12345', '5']
+ }
+ end
+
+ let(:new_wal_location_with_offset) do
+ {
+ # offset is relative to `existing_wal`
+ main: ['0/D525E3A2', '2'],
+ ci: ['AB/12342', '2']
+ }
+ end
it "does not update a wal location to redis with an offset" do
expect { duplicate_job.update_latest_wal_location! }
.to not_change { read_range_from_redis(wal_location_key(idempotency_key, :main)) }
- .from([existing_wal_locations[:main], existing_offset])
+ .from(stored_wal_location_with_offset[:main])
.and not_change { read_range_from_redis(wal_location_key(idempotency_key, :ci)) }
- .from([existing_wal_locations[:ci], existing_offset])
+ .from(stored_wal_location_with_offset[:ci])
end
end
end
@@ -270,6 +352,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
context 'when the key exists in redis' do
before do
set_idempotency_key(idempotency_key, 'existing-jid')
+ set_idempotency_key(deduplicated_flag_key, 1)
wal_locations.each do |config_name, location|
set_idempotency_key(existing_wal_location_key(idempotency_key, config_name), location)
set_idempotency_key(wal_location_key(idempotency_key, config_name), location)
@@ -299,6 +382,11 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
let(:from_value) { 'existing-jid' }
end
+ it_behaves_like 'deleting keys from redis', 'deduplication counter key' do
+ let(:key) { deduplicated_flag_key }
+ let(:from_value) { '1' }
+ end
+
it_behaves_like 'deleting keys from redis', 'existing wal location keys for main database' do
let(:key) { existing_wal_location_key(idempotency_key, :main) }
let(:from_value) { wal_locations[:main] }
@@ -390,6 +478,103 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
end
end
+ describe '#reschedule' do
+ it 'reschedules the current job' do
+ fake_logger = instance_double(Gitlab::SidekiqLogging::DeduplicationLogger)
+ expect(Gitlab::SidekiqLogging::DeduplicationLogger).to receive(:instance).and_return(fake_logger)
+ expect(fake_logger).to receive(:rescheduled_log).with(a_hash_including({ 'jid' => '123' }))
+ expect(AuthorizedProjectsWorker).to receive(:perform_async).with(1).once
+
+ duplicate_job.reschedule
+ end
+ end
+
+ describe '#should_reschedule?' do
+ subject { duplicate_job.should_reschedule? }
+
+ context 'when the job is reschedulable' do
+ before do
+ allow(duplicate_job).to receive(:reschedulable?) { true }
+ end
+
+ it { is_expected.to eq(false) }
+
+ context 'with deduplicated flag' do
+ before do
+ duplicate_job.set_deduplicated_flag!
+ end
+
+ it { is_expected.to eq(true) }
+ end
+ end
+
+ context 'when the job is not reschedulable' do
+ before do
+ allow(duplicate_job).to receive(:reschedulable?) { false }
+ end
+
+ it { is_expected.to eq(false) }
+
+ context 'with deduplicated flag' do
+ before do
+ duplicate_job.set_deduplicated_flag!
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+ end
+
+ describe '#set_deduplicated_flag!' do
+ context 'when the job is reschedulable' do
+ before do
+ allow(duplicate_job).to receive(:reschedulable?) { true }
+ end
+
+ it 'sets the key in Redis' do
+ duplicate_job.set_deduplicated_flag!
+
+ flag = Sidekiq.redis { |redis| redis.get(deduplicated_flag_key) }
+
+ expect(flag).to eq(described_class::DEDUPLICATED_FLAG_VALUE.to_s)
+ end
+
+ it 'sets, gets and cleans up the deduplicated flag' do
+ expect(duplicate_job.should_reschedule?).to eq(false)
+
+ duplicate_job.set_deduplicated_flag!
+ expect(duplicate_job.should_reschedule?).to eq(true)
+
+ duplicate_job.delete!
+ expect(duplicate_job.should_reschedule?).to eq(false)
+ end
+ end
+
+ context 'when the job is not reschedulable' do
+ before do
+ allow(duplicate_job).to receive(:reschedulable?) { false }
+ end
+
+ it 'does not set the key in Redis' do
+ duplicate_job.set_deduplicated_flag!
+
+ flag = Sidekiq.redis { |redis| redis.get(deduplicated_flag_key) }
+
+ expect(flag).to be_nil
+ end
+
+ it 'does not set the deduplicated flag' do
+ expect(duplicate_job.should_reschedule?).to eq(false)
+
+ duplicate_job.set_deduplicated_flag!
+ expect(duplicate_job.should_reschedule?).to eq(false)
+
+ duplicate_job.delete!
+ expect(duplicate_job.should_reschedule?).to eq(false)
+ end
+ end
+ end
+
describe '#duplicate?' do
it "raises an error if the check wasn't performed" do
expect { duplicate_job.duplicate? }.to raise_error /Call `#check!` first/
@@ -494,12 +679,12 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
end
end
- def existing_wal_location_key(idempotency_key, config_name)
- "#{idempotency_key}:#{config_name}:existing_wal_location"
+ def existing_wal_location_key(idempotency_key, connection_name)
+ "#{idempotency_key}:#{connection_name}:existing_wal_location"
end
- def wal_location_key(idempotency_key, config_name)
- "#{idempotency_key}:#{config_name}:wal_location"
+ def wal_location_key(idempotency_key, connection_name)
+ "#{idempotency_key}:#{connection_name}:wal_location"
end
def set_idempotency_key(key, value = '1')
diff --git a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/until_executed_spec.rb b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/until_executed_spec.rb
index 9772255fc50..963301bc001 100644
--- a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/until_executed_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/until_executed_spec.rb
@@ -9,6 +9,9 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::Strategies::UntilExecut
before do
allow(fake_duplicate_job).to receive(:latest_wal_locations).and_return( {} )
+ allow(fake_duplicate_job).to receive(:scheduled?) { false }
+ allow(fake_duplicate_job).to receive(:options) { {} }
+ allow(fake_duplicate_job).to receive(:should_reschedule?) { false }
end
it 'deletes the lock after executing' do
@@ -19,6 +22,28 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::Strategies::UntilExecut
proc.call
end
end
+
+ it 'does not reschedule the job even if deduplication happened' do
+ expect(fake_duplicate_job).to receive(:delete!)
+ expect(fake_duplicate_job).not_to receive(:reschedule)
+
+ strategy.perform({}) do
+ proc.call
+ end
+ end
+
+ context 'when job is reschedulable' do
+ it 'reschedules the job if deduplication happened' do
+ allow(fake_duplicate_job).to receive(:should_reschedule?) { true }
+
+ expect(fake_duplicate_job).to receive(:delete!)
+ expect(fake_duplicate_job).to receive(:reschedule).once
+
+ strategy.perform({}) do
+ proc.call
+ end
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/sidekiq_middleware/query_analyzer_spec.rb b/spec/lib/gitlab/sidekiq_middleware/query_analyzer_spec.rb
new file mode 100644
index 00000000000..e58af1d60fe
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_middleware/query_analyzer_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::SidekiqMiddleware::QueryAnalyzer, query_analyzers: false do
+ describe 'the PreventCrossDatabaseModification' do
+ describe '#call' do
+ let(:worker) { double(:worker) }
+ let(:job) { { 'jid' => 'job123' } }
+ let(:queue) { 'some-queue' }
+ let(:middleware) { described_class.new }
+
+ def do_queries
+ end
+
+ subject { middleware.call(worker, job, queue) { do_queries } }
+
+ context 'when there is a cross modification' do
+ def do_queries
+ Project.transaction do
+ Project.where(id: -1).update_all(id: -1)
+ ::Ci::Pipeline.where(id: -1).update_all(id: -1)
+ end
+ end
+
+ it 'detects cross modifications and tracks exception' do
+ expect(::Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
+
+ subject
+ end
+
+ context 'when the detect_cross_database_modification is disabled' do
+ before do
+ stub_feature_flags(detect_cross_database_modification: false)
+ end
+
+ it 'does not detect cross modifications' do
+ expect(::Gitlab::ErrorTracking).not_to receive(:track_and_raise_for_dev_exception)
+
+ subject
+ end
+ end
+ end
+
+ context 'when there is no cross modification' do
+ def do_queries
+ Project.transaction do
+ Project.where(id: -1).update_all(id: -1)
+ Namespace.where(id: -1).update_all(id: -1)
+ end
+ end
+
+ it 'does not log anything' do
+ expect(::Gitlab::ErrorTracking).not_to receive(:track_and_raise_for_dev_exception)
+
+ subject
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sidekiq_middleware/size_limiter/validator_spec.rb b/spec/lib/gitlab/sidekiq_middleware/size_limiter/validator_spec.rb
index 3a6fdd7642c..876069a1a92 100644
--- a/spec/lib/gitlab/sidekiq_middleware/size_limiter/validator_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/size_limiter/validator_spec.rb
@@ -59,111 +59,6 @@ RSpec.describe Gitlab::SidekiqMiddleware::SizeLimiter::Validator, :aggregate_fai
expect(validator.size_limit).to eq(2)
end
end
-
- context 'when the input mode is valid' do
- it 'does not log a warning message' do
- expect(::Sidekiq.logger).not_to receive(:warn)
-
- described_class.new(TestSizeLimiterWorker, job_payload, mode: 'track')
- described_class.new(TestSizeLimiterWorker, job_payload, mode: 'compress')
- end
- end
-
- context 'when the input mode is invalid' do
- it 'defaults to track mode and logs a warning message' do
- expect(::Sidekiq.logger).to receive(:warn).with('Invalid Sidekiq size limiter mode: invalid. Fallback to track mode.')
-
- validator = described_class.new(TestSizeLimiterWorker, job_payload, mode: 'invalid')
-
- expect(validator.mode).to eql('track')
- end
- end
-
- context 'when the input mode is empty' do
- it 'defaults to track mode' do
- expect(::Sidekiq.logger).not_to receive(:warn)
-
- validator = described_class.new(TestSizeLimiterWorker, job_payload, mode: nil)
-
- expect(validator.mode).to eql('track')
- end
- end
-
- context 'when the size input is valid' do
- it 'does not log a warning message' do
- expect(::Sidekiq.logger).not_to receive(:warn)
-
- described_class.new(TestSizeLimiterWorker, job_payload, size_limit: 300)
- described_class.new(TestSizeLimiterWorker, job_payload, size_limit: 0)
- end
- end
-
- context 'when the size input is invalid' do
- it 'logs a warning message' do
- expect(::Sidekiq.logger).to receive(:warn).with('Invalid Sidekiq size limiter limit: -1')
-
- validator = described_class.new(TestSizeLimiterWorker, job_payload, size_limit: -1)
-
- expect(validator.size_limit).to be(0)
- end
- end
-
- context 'when the size input is empty' do
- it 'defaults to 0' do
- expect(::Sidekiq.logger).not_to receive(:warn)
-
- validator = described_class.new(TestSizeLimiterWorker, job_payload, size_limit: nil)
-
- expect(validator.size_limit).to be(described_class::DEFAULT_SIZE_LIMIT)
- end
- end
-
- context 'when the compression threshold is valid' do
- it 'does not log a warning message' do
- expect(::Sidekiq.logger).not_to receive(:warn)
-
- described_class.new(TestSizeLimiterWorker, job_payload, compression_threshold: 300)
- described_class.new(TestSizeLimiterWorker, job_payload, compression_threshold: 1)
- end
- end
-
- context 'when the compression threshold is negative' do
- it 'logs a warning message' do
- expect(::Sidekiq.logger).to receive(:warn).with('Invalid Sidekiq size limiter compression threshold: -1')
-
- described_class.new(TestSizeLimiterWorker, job_payload, compression_threshold: -1)
- end
-
- it 'falls back to the default' do
- validator = described_class.new(TestSizeLimiterWorker, job_payload, compression_threshold: -1)
-
- expect(validator.compression_threshold).to be(100_000)
- end
- end
-
- context 'when the compression threshold is zero' do
- it 'logs a warning message' do
- expect(::Sidekiq.logger).to receive(:warn).with('Invalid Sidekiq size limiter compression threshold: 0')
-
- described_class.new(TestSizeLimiterWorker, job_payload, compression_threshold: 0)
- end
-
- it 'falls back to the default' do
- validator = described_class.new(TestSizeLimiterWorker, job_payload, compression_threshold: 0)
-
- expect(validator.compression_threshold).to be(100_000)
- end
- end
-
- context 'when the compression threshold is empty' do
- it 'defaults to 100_000' do
- expect(::Sidekiq.logger).not_to receive(:warn)
-
- validator = described_class.new(TestSizeLimiterWorker, job_payload)
-
- expect(validator.compression_threshold).to be(100_000)
- end
- end
end
shared_examples 'validate limit job payload size' do
@@ -171,20 +66,6 @@ RSpec.describe Gitlab::SidekiqMiddleware::SizeLimiter::Validator, :aggregate_fai
let(:compression_threshold) { nil }
let(:mode) { 'track' }
- context 'when size limit negative' do
- let(:size_limit) { -1 }
-
- it 'does not track jobs' do
- expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
-
- validate.call(TestSizeLimiterWorker, job_payload(a: 'a' * 300))
- end
-
- it 'does not raise exception' do
- expect { validate.call(TestSizeLimiterWorker, job_payload(a: 'a' * 300)) }.not_to raise_error
- end
- end
-
context 'when size limit is 0' do
let(:size_limit) { 0 }
let(:job) { job_payload(a: 'a' * 300) }
@@ -438,36 +319,20 @@ RSpec.describe Gitlab::SidekiqMiddleware::SizeLimiter::Validator, :aggregate_fai
end
describe '#validate!' do
- context 'when creating an instance with the related configuration variables' do
- let(:validate) do
- ->(worker_clas, job) do
- described_class.new(worker_class, job).validate!
- end
+ let(:validate) do
+ ->(worker_class, job) do
+ described_class.new(worker_class, job).validate!
end
-
- before do
- stub_application_setting(
- sidekiq_job_limiter_mode: mode,
- sidekiq_job_limiter_compression_threshold_bytes: compression_threshold,
- sidekiq_job_limiter_limit_bytes: size_limit
- )
- end
-
- it_behaves_like 'validate limit job payload size'
end
- context 'when creating an instance with mode and size limit' do
- let(:validate) do
- ->(worker_clas, job) do
- validator = described_class.new(
- worker_class, job,
- mode: mode, size_limit: size_limit, compression_threshold: compression_threshold
- )
- validator.validate!
- end
- end
-
- it_behaves_like 'validate limit job payload size'
+ before do
+ stub_application_setting(
+ sidekiq_job_limiter_mode: mode,
+ sidekiq_job_limiter_compression_threshold_bytes: compression_threshold,
+ sidekiq_job_limiter_limit_bytes: size_limit
+ )
end
+
+ it_behaves_like 'validate limit job payload size'
end
end
diff --git a/spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb b/spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb
index 92a11c83a4a..b9a13fd697e 100644
--- a/spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Client do
include ApplicationWorker
- feature_category :issue_tracking
+ feature_category :team_planning
def self.job_for_args(args)
jobs.find { |job| job['args'] == args }
@@ -78,8 +78,8 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Client do
job1 = TestWithContextWorker.job_for_args(['job1', 1, 2, 3])
job2 = TestWithContextWorker.job_for_args(['job2', 1, 2, 3])
- expect(job1['meta.feature_category']).to eq('issue_tracking')
- expect(job2['meta.feature_category']).to eq('issue_tracking')
+ expect(job1['meta.feature_category']).to eq('team_planning')
+ expect(job2['meta.feature_category']).to eq('team_planning')
end
it 'takes the feature category from the caller if the worker is not owned' do
@@ -116,8 +116,8 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Client do
job1 = TestWithContextWorker.job_for_args(['job1', 1, 2, 3])
job2 = TestWithContextWorker.job_for_args(['job2', 1, 2, 3])
- expect(job1['meta.feature_category']).to eq('issue_tracking')
- expect(job2['meta.feature_category']).to eq('issue_tracking')
+ expect(job1['meta.feature_category']).to eq('team_planning')
+ expect(job2['meta.feature_category']).to eq('team_planning')
end
it 'takes the feature category from the caller if the worker is not owned' do
diff --git a/spec/lib/gitlab/spamcheck/client_spec.rb b/spec/lib/gitlab/spamcheck/client_spec.rb
index 15e963fe423..e542ce455bb 100644
--- a/spec/lib/gitlab/spamcheck/client_spec.rb
+++ b/spec/lib/gitlab/spamcheck/client_spec.rb
@@ -82,7 +82,7 @@ RSpec.describe Gitlab::Spamcheck::Client do
end
end
- describe '#build_user_proto_buf', :aggregate_failures do
+ describe '#build_user_protobuf', :aggregate_failures do
it 'builds the expected protobuf object' do
user_pb = described_class.new.send(:build_user_protobuf, user)
expect(user_pb.username).to eq user.username
diff --git a/spec/lib/gitlab/subscription_portal_spec.rb b/spec/lib/gitlab/subscription_portal_spec.rb
index a3808b0f0e2..4be1c85f7c8 100644
--- a/spec/lib/gitlab/subscription_portal_spec.rb
+++ b/spec/lib/gitlab/subscription_portal_spec.rb
@@ -9,14 +9,13 @@ RSpec.describe ::Gitlab::SubscriptionPortal do
before do
stub_env('CUSTOMER_PORTAL_URL', env_value)
- stub_feature_flags(new_customersdot_staging_url: false)
end
describe '.default_subscriptions_url' do
where(:test, :development, :result) do
false | false | 'https://customers.gitlab.com'
- false | true | 'https://customers.stg.gitlab.com'
- true | false | 'https://customers.stg.gitlab.com'
+ false | true | 'https://customers.staging.gitlab.com'
+ true | false | 'https://customers.staging.gitlab.com'
end
before do
@@ -35,7 +34,7 @@ RSpec.describe ::Gitlab::SubscriptionPortal do
subject { described_class.subscriptions_url }
context 'when CUSTOMER_PORTAL_URL ENV is unset' do
- it { is_expected.to eq('https://customers.stg.gitlab.com') }
+ it { is_expected.to eq('https://customers.staging.gitlab.com') }
end
context 'when CUSTOMER_PORTAL_URL ENV is set' do
@@ -55,15 +54,15 @@ RSpec.describe ::Gitlab::SubscriptionPortal do
context 'url methods' do
where(:method_name, :result) do
- :default_subscriptions_url | 'https://customers.stg.gitlab.com'
- :payment_form_url | 'https://customers.stg.gitlab.com/payment_forms/cc_validation'
- :subscriptions_graphql_url | 'https://customers.stg.gitlab.com/graphql'
- :subscriptions_more_minutes_url | 'https://customers.stg.gitlab.com/buy_pipeline_minutes'
- :subscriptions_more_storage_url | 'https://customers.stg.gitlab.com/buy_storage'
- :subscriptions_manage_url | 'https://customers.stg.gitlab.com/subscriptions'
- :subscriptions_plans_url | 'https://customers.stg.gitlab.com/plans'
- :subscriptions_instance_review_url | 'https://customers.stg.gitlab.com/instance_review'
- :subscriptions_gitlab_plans_url | 'https://customers.stg.gitlab.com/gitlab_plans'
+ :default_subscriptions_url | 'https://customers.staging.gitlab.com'
+ :payment_form_url | 'https://customers.staging.gitlab.com/payment_forms/cc_validation'
+ :subscriptions_graphql_url | 'https://customers.staging.gitlab.com/graphql'
+ :subscriptions_more_minutes_url | 'https://customers.staging.gitlab.com/buy_pipeline_minutes'
+ :subscriptions_more_storage_url | 'https://customers.staging.gitlab.com/buy_storage'
+ :subscriptions_manage_url | 'https://customers.staging.gitlab.com/subscriptions'
+ :subscriptions_plans_url | 'https://about.gitlab.com/pricing/'
+ :subscriptions_instance_review_url | 'https://customers.staging.gitlab.com/instance_review'
+ :subscriptions_gitlab_plans_url | 'https://customers.staging.gitlab.com/gitlab_plans'
end
with_them do
@@ -78,7 +77,7 @@ RSpec.describe ::Gitlab::SubscriptionPortal do
let(:group_id) { 153 }
- it { is_expected.to eq("https://customers.stg.gitlab.com/gitlab/namespaces/#{group_id}/extra_seats") }
+ it { is_expected.to eq("https://customers.staging.gitlab.com/gitlab/namespaces/#{group_id}/extra_seats") }
end
describe '.upgrade_subscription_url' do
@@ -87,7 +86,7 @@ RSpec.describe ::Gitlab::SubscriptionPortal do
let(:group_id) { 153 }
let(:plan_id) { 5 }
- it { is_expected.to eq("https://customers.stg.gitlab.com/gitlab/namespaces/#{group_id}/upgrade/#{plan_id}") }
+ it { is_expected.to eq("https://customers.staging.gitlab.com/gitlab/namespaces/#{group_id}/upgrade/#{plan_id}") }
end
describe '.renew_subscription_url' do
@@ -95,6 +94,6 @@ RSpec.describe ::Gitlab::SubscriptionPortal do
let(:group_id) { 153 }
- it { is_expected.to eq("https://customers.stg.gitlab.com/gitlab/namespaces/#{group_id}/renew") }
+ it { is_expected.to eq("https://customers.staging.gitlab.com/gitlab/namespaces/#{group_id}/renew") }
end
end
diff --git a/spec/lib/gitlab/tracking/destinations/product_analytics_spec.rb b/spec/lib/gitlab/tracking/destinations/product_analytics_spec.rb
deleted file mode 100644
index 63e2e930acd..00000000000
--- a/spec/lib/gitlab/tracking/destinations/product_analytics_spec.rb
+++ /dev/null
@@ -1,84 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Tracking::Destinations::ProductAnalytics do
- let(:emitter) { SnowplowTracker::Emitter.new('localhost', buffer_size: 1) }
- let(:tracker) { SnowplowTracker::Tracker.new(emitter, SnowplowTracker::Subject.new, 'namespace', 'app_id') }
-
- describe '#event' do
- shared_examples 'does not send an event' do
- it 'does not send an event' do
- expect_any_instance_of(SnowplowTracker::Tracker).not_to receive(:track_struct_event)
-
- subject.event(allowed_category, allowed_action)
- end
- end
-
- let(:allowed_category) { 'epics' }
- let(:allowed_action) { 'promote' }
- let(:self_monitoring_project) { create(:project) }
-
- before do
- stub_feature_flags(product_analytics_tracking: true)
- stub_application_setting(self_monitoring_project_id: self_monitoring_project.id)
- stub_application_setting(usage_ping_enabled: true)
- end
-
- context 'with allowed event' do
- it 'sends an event to Product Analytics snowplow collector' do
- expect(SnowplowTracker::AsyncEmitter)
- .to receive(:new)
- .with(ProductAnalytics::Tracker::COLLECTOR_URL, protocol: Gitlab.config.gitlab.protocol)
- .and_return(emitter)
-
- expect(SnowplowTracker::Tracker)
- .to receive(:new)
- .with(emitter, an_instance_of(SnowplowTracker::Subject), Gitlab::Tracking::SNOWPLOW_NAMESPACE, self_monitoring_project.id.to_s)
- .and_return(tracker)
-
- freeze_time do
- expect(tracker)
- .to receive(:track_struct_event)
- .with(allowed_category, allowed_action, 'label', 'property', 1.5, nil, (Time.now.to_f * 1000).to_i)
-
- subject.event(allowed_category, allowed_action, label: 'label', property: 'property', value: 1.5)
- end
- end
- end
-
- context 'with non-allowed event' do
- it 'does not send an event' do
- expect_any_instance_of(SnowplowTracker::Tracker).not_to receive(:track_struct_event)
-
- subject.event('category', 'action')
- subject.event(allowed_category, 'action')
- subject.event('category', allowed_action)
- end
- end
-
- context 'when self-monitoring project does not exist' do
- before do
- stub_application_setting(self_monitoring_project_id: nil)
- end
-
- include_examples 'does not send an event'
- end
-
- context 'when product_analytics_tracking FF is disabled' do
- before do
- stub_feature_flags(product_analytics_tracking: false)
- end
-
- include_examples 'does not send an event'
- end
-
- context 'when usage ping is disabled' do
- before do
- stub_application_setting(usage_ping_enabled: false)
- end
-
- include_examples 'does not send an event'
- end
- end
-end
diff --git a/spec/lib/gitlab/tracking/destinations/snowplow_micro_spec.rb b/spec/lib/gitlab/tracking/destinations/snowplow_micro_spec.rb
new file mode 100644
index 00000000000..6004698d092
--- /dev/null
+++ b/spec/lib/gitlab/tracking/destinations/snowplow_micro_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Tracking::Destinations::SnowplowMicro do
+ include StubENV
+
+ before do
+ stub_application_setting(snowplow_enabled: true)
+ stub_env('SNOWPLOW_MICRO_ENABLE', '1')
+ allow(Rails.env).to receive(:development?).and_return(true)
+ end
+
+ describe '#hostname' do
+ context 'when SNOWPLOW_MICRO_URI is set' do
+ before do
+ stub_env('SNOWPLOW_MICRO_URI', 'http://gdk.test:9091')
+ end
+
+ it 'returns hostname URI part' do
+ expect(subject.hostname).to eq('gdk.test:9091')
+ end
+ end
+
+ context 'when SNOWPLOW_MICRO_URI is without protocol' do
+ before do
+ stub_env('SNOWPLOW_MICRO_URI', 'gdk.test:9091')
+ end
+
+ it 'returns hostname URI part' do
+ expect(subject.hostname).to eq('gdk.test:9091')
+ end
+ end
+
+ context 'when SNOWPLOW_MICRO_URI is hostname only' do
+ before do
+ stub_env('SNOWPLOW_MICRO_URI', 'uriwithoutport')
+ end
+
+ it 'returns hostname URI with default HTTP port' do
+ expect(subject.hostname).to eq('uriwithoutport:80')
+ end
+ end
+
+ context 'when SNOWPLOW_MICRO_URI is not set' do
+ it 'returns localhost hostname' do
+ expect(subject.hostname).to eq('localhost:9090')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/tracking/standard_context_spec.rb b/spec/lib/gitlab/tracking/standard_context_spec.rb
index 8ded80dd191..7d678db5ec8 100644
--- a/spec/lib/gitlab/tracking/standard_context_spec.rb
+++ b/spec/lib/gitlab/tracking/standard_context_spec.rb
@@ -99,25 +99,5 @@ RSpec.describe Gitlab::Tracking::StandardContext do
it 'accepts just project id as integer' do
expect { described_class.new(project: 1).to_context }.not_to raise_error
end
-
- context 'without add_namespace_and_project_to_snowplow_tracking feature' do
- before do
- stub_feature_flags(add_namespace_and_project_to_snowplow_tracking: false)
- end
-
- it 'does not contain project or namespace ids' do
- expect(snowplow_context.to_json[:data].keys).not_to include(:project_id, :namespace_id)
- end
- end
-
- context 'without add_actor_based_user_to_snowplow_tracking feature' do
- before do
- stub_feature_flags(add_actor_based_user_to_snowplow_tracking: false)
- end
-
- it 'does not contain user_id' do
- expect(snowplow_context.to_json[:data].keys).not_to include(:user_id)
- end
- end
end
end
diff --git a/spec/lib/gitlab/tracking_spec.rb b/spec/lib/gitlab/tracking_spec.rb
index dacaae55676..61b2c89ffa1 100644
--- a/spec/lib/gitlab/tracking_spec.rb
+++ b/spec/lib/gitlab/tracking_spec.rb
@@ -2,6 +2,8 @@
require 'spec_helper'
RSpec.describe Gitlab::Tracking do
+ include StubENV
+
before do
stub_application_setting(snowplow_enabled: true)
stub_application_setting(snowplow_collector_hostname: 'gitfoo.com')
@@ -12,17 +14,62 @@ RSpec.describe Gitlab::Tracking do
end
describe '.options' do
- it 'returns useful client options' do
- expected_fields = {
- namespace: 'gl',
- hostname: 'gitfoo.com',
- cookieDomain: '.gitfoo.com',
- appId: '_abc123_',
- formTracking: true,
- linkClickTracking: true
- }
-
- expect(subject.options(nil)).to match(expected_fields)
+ shared_examples 'delegates to destination' do |klass|
+ before do
+ allow_next_instance_of(klass) do |instance|
+ allow(instance).to receive(:options).and_call_original
+ end
+ end
+
+ it "delegates to #{klass} destination" do
+ expect_next_instance_of(klass) do |instance|
+ expect(instance).to receive(:options)
+ end
+
+ subject.options(nil)
+ end
+ end
+
+ context 'when destination is Snowplow' do
+ it_behaves_like 'delegates to destination', Gitlab::Tracking::Destinations::Snowplow
+
+ it 'returns useful client options' do
+ expected_fields = {
+ namespace: 'gl',
+ hostname: 'gitfoo.com',
+ cookieDomain: '.gitfoo.com',
+ appId: '_abc123_',
+ formTracking: true,
+ linkClickTracking: true
+ }
+
+ expect(subject.options(nil)).to match(expected_fields)
+ end
+ end
+
+ context 'when destination is SnowplowMicro' do
+ before do
+ stub_env('SNOWPLOW_MICRO_ENABLE', '1')
+ allow(Rails.env).to receive(:development?).and_return(true)
+ end
+
+ it_behaves_like 'delegates to destination', Gitlab::Tracking::Destinations::SnowplowMicro
+
+ it 'returns useful client options' do
+ expected_fields = {
+ namespace: 'gl',
+ hostname: 'localhost:9090',
+ cookieDomain: '.gitlab.com',
+ appId: '_abc123_',
+ protocol: 'http',
+ port: 9090,
+ force_secure_tracker: false,
+ formTracking: true,
+ linkClickTracking: true
+ }
+
+ expect(subject.options(nil)).to match(expected_fields)
+ end
end
it 'when feature flag is disabled' do
@@ -41,7 +88,6 @@ RSpec.describe Gitlab::Tracking do
shared_examples 'delegates to destination' do |klass|
before do
allow_any_instance_of(Gitlab::Tracking::Destinations::Snowplow).to receive(:event)
- allow_any_instance_of(Gitlab::Tracking::Destinations::ProductAnalytics).to receive(:event)
end
it "delegates to #{klass} destination" do
@@ -72,8 +118,23 @@ RSpec.describe Gitlab::Tracking do
end
end
- it_behaves_like 'delegates to destination', Gitlab::Tracking::Destinations::Snowplow
- it_behaves_like 'delegates to destination', Gitlab::Tracking::Destinations::ProductAnalytics
+ context 'when destination is Snowplow' do
+ before do
+ stub_env('SNOWPLOW_MICRO_ENABLE', '0')
+ allow(Rails.env).to receive(:development?).and_return(true)
+ end
+
+ it_behaves_like 'delegates to destination', Gitlab::Tracking::Destinations::Snowplow
+ end
+
+ context 'when destination is SnowplowMicro' do
+ before do
+ stub_env('SNOWPLOW_MICRO_ENABLE', '1')
+ allow(Rails.env).to receive(:development?).and_return(true)
+ end
+
+ it_behaves_like 'delegates to destination', Gitlab::Tracking::Destinations::SnowplowMicro
+ end
it 'tracks errors' do
expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).with(
diff --git a/spec/lib/gitlab/usage/metric_definition_spec.rb b/spec/lib/gitlab/usage/metric_definition_spec.rb
index 522f69062fb..a22b3a733bd 100644
--- a/spec/lib/gitlab/usage/metric_definition_spec.rb
+++ b/spec/lib/gitlab/usage/metric_definition_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe Gitlab::Usage::MetricDefinition do
value_type: 'string',
product_category: 'collection',
product_stage: 'growth',
+ product_section: 'devops',
status: 'active',
milestone: '14.1',
default_generation: 'generation_1',
@@ -222,6 +223,7 @@ RSpec.describe Gitlab::Usage::MetricDefinition do
value_type: 'string',
product_category: 'collection',
product_stage: 'growth',
+ product_section: 'devops',
status: 'active',
milestone: '14.1',
default_generation: 'generation_1',
diff --git a/spec/lib/gitlab/usage/metric_spec.rb b/spec/lib/gitlab/usage/metric_spec.rb
index ea8d1a135a6..19d2d3048eb 100644
--- a/spec/lib/gitlab/usage/metric_spec.rb
+++ b/spec/lib/gitlab/usage/metric_spec.rb
@@ -45,4 +45,10 @@ RSpec.describe Gitlab::Usage::Metric do
expect(described_class.new(issue_count_metric_definiton).with_instrumentation).to eq({ counts: { issues: "SELECT COUNT(\"issues\".\"id\") FROM \"issues\"" } })
end
end
+
+ describe '#with_suggested_name' do
+ it 'returns key_path metric with the corresponding generated query' do
+ expect(described_class.new(issue_count_metric_definiton).with_suggested_name).to eq({ counts: { issues: 'count_issues' } })
+ end
+ end
end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/generic_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/generic_metric_spec.rb
index 158be34d39c..c8cb1bb4373 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/generic_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/generic_metric_spec.rb
@@ -7,18 +7,18 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::GenericMetric do
subject do
Class.new(described_class) do
fallback(custom_fallback)
- value { Gitlab::Database.main.version }
+ value { ApplicationRecord.database.version }
end.new(time_frame: 'none')
end
describe '#value' do
it 'gives the correct value' do
- expect(subject.value).to eq(Gitlab::Database.main.version)
+ expect(subject.value).to eq(ApplicationRecord.database.version)
end
context 'when raising an exception' do
it 'return the custom fallback' do
- expect(Gitlab::Database.main).to receive(:version).and_raise('Error')
+ expect(ApplicationRecord.database).to receive(:version).and_raise('Error')
expect(subject.value).to eq(custom_fallback)
end
end
@@ -28,18 +28,18 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::GenericMetric do
context 'with default fallback' do
subject do
Class.new(described_class) do
- value { Gitlab::Database.main.version }
+ value { ApplicationRecord.database.version }
end.new(time_frame: 'none')
end
describe '#value' do
it 'gives the correct value' do
- expect(subject.value).to eq(Gitlab::Database.main.version )
+ expect(subject.value).to eq(ApplicationRecord.database.version )
end
context 'when raising an exception' do
it 'return the default fallback' do
- expect(Gitlab::Database.main).to receive(:version).and_raise('Error')
+ expect(ApplicationRecord.database).to receive(:version).and_raise('Error')
expect(subject.value).to eq(described_class::FALLBACK)
end
end
diff --git a/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb b/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb
index 0f95da74ff9..dbbc718e147 100644
--- a/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb
@@ -25,10 +25,30 @@ RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator do
end
context 'for count with default column metrics' do
- it_behaves_like 'name suggestion' do
- # corresponding metric is collected with count(Board)
- let(:key_path) { 'counts.boards' }
- let(:name_suggestion) { /count_boards/ }
+ context 'with usage_data_instrumentation feature flag' do
+ context 'when enabled' do
+ before do
+ stub_feature_flags(usage_data_instrumentation: true)
+ end
+
+ it_behaves_like 'name suggestion' do
+ # corresponding metric is collected with ::Gitlab::UsageDataMetrics.suggested_names
+ let(:key_path) { 'counts.boards' }
+ let(:name_suggestion) { /count_boards/ }
+ end
+ end
+
+ context 'when disabled' do
+ before do
+ stub_feature_flags(usage_data_instrumentation: false)
+ end
+
+ it_behaves_like 'name suggestion' do
+ # corresponding metric is collected with count(Board)
+ let(:key_path) { 'counts.boards' }
+ let(:name_suggestion) { /count_boards/ }
+ 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/vscode_extenion_activity_unique_counter_spec.rb
index 7593d51fe76..7593d51fe76 100644
--- a/spec/lib/gitlab/usage_data_counters/vs_code_extenion_activity_unique_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/vscode_extenion_activity_unique_counter_spec.rb
diff --git a/spec/lib/gitlab/usage_data_metrics_spec.rb b/spec/lib/gitlab/usage_data_metrics_spec.rb
index ee0cfb1407e..563eed75c38 100644
--- a/spec/lib/gitlab/usage_data_metrics_spec.rb
+++ b/spec/lib/gitlab/usage_data_metrics_spec.rb
@@ -13,7 +13,9 @@ RSpec.describe Gitlab::UsageDataMetrics do
end
before do
- allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
+ allow_next_instance_of(Gitlab::Database::BatchCounter) do |batch_counter|
+ allow(batch_counter).to receive(:transaction_open?).and_return(false)
+ end
end
context 'with instrumentation_class' do
@@ -76,4 +78,16 @@ RSpec.describe Gitlab::UsageDataMetrics do
end
end
end
+
+ describe '.suggested_names' do
+ subject { described_class.suggested_names }
+
+ let(:suggested_names) do
+ ::Gitlab::Usage::Metric.all.map(&:with_suggested_name).reduce({}, :deep_merge)
+ end
+
+ it 'includes Service Ping suggested names' do
+ expect(subject).to match_array(suggested_names)
+ end
+ end
end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 833bf260019..cf544c07195 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -80,6 +80,12 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
end
end
end
+
+ it 'allows indifferent access' do
+ allow(::Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:unique_events).and_return(1)
+ expect(subject[:search_unique_visits][:search_unique_visits_for_any_target_monthly]).to eq(1)
+ expect(subject[:search_unique_visits]['search_unique_visits_for_any_target_monthly']).to eq(1)
+ end
end
describe 'usage_activity_by_stage_package' do
@@ -187,6 +193,8 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
end
describe 'usage_activity_by_stage_manage' do
+ let_it_be(:error_rate) { Gitlab::Database::PostgresHll::BatchDistinctCounter::ERROR_RATE }
+
it 'includes accurate usage_activity_by_stage data' do
stub_config(
omniauth:
@@ -207,14 +215,14 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
end
expect(described_class.usage_activity_by_stage_manage({})).to include(
- events: 2,
+ events: -1,
groups: 2,
users_created: 6,
omniauth_providers: ['google_oauth2'],
user_auth_by_provider: { 'group_saml' => 2, 'ldap' => 4, 'standard' => 0, 'two-factor' => 0, 'two-factor-via-u2f-device' => 0, "two-factor-via-webauthn-device" => 0 }
)
expect(described_class.usage_activity_by_stage_manage(described_class.monthly_time_range_db_params)).to include(
- events: 1,
+ events: be_within(error_rate).percent_of(1),
groups: 1,
users_created: 3,
omniauth_providers: ['google_oauth2'],
@@ -367,9 +375,9 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
def omniauth_providers
[
- OpenStruct.new(name: 'google_oauth2'),
- OpenStruct.new(name: 'ldapmain'),
- OpenStruct.new(name: 'group_saml')
+ double('provider', name: 'google_oauth2'),
+ double('provider', name: 'ldapmain'),
+ double('provider', name: 'group_saml')
]
end
end
@@ -428,7 +436,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
end
expect(described_class.usage_activity_by_stage_plan({})).to include(
- issues: 3,
notes: 2,
projects: 2,
todos: 2,
@@ -439,7 +446,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
projects_jira_dvcs_server_active: 2
)
expect(described_class.usage_activity_by_stage_plan(described_class.monthly_time_range_db_params)).to include(
- issues: 2,
notes: 1,
projects: 1,
todos: 1,
@@ -450,6 +456,44 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
projects_jira_dvcs_server_active: 1
)
end
+
+ context 'with usage_data_instrumentation feature flag' do
+ context 'when enabled' do
+ it 'merges the data from instrumentation classes' do
+ stub_feature_flags(usage_data_instrumentation: true)
+
+ for_defined_days_back do
+ user = create(:user)
+ project = create(:project, creator: user)
+ create(:issue, project: project, author: user)
+ create(:issue, project: project, author: User.support_bot)
+ end
+
+ expect(described_class.usage_activity_by_stage_plan({})).to include(issues: Gitlab::Utils::UsageData::INSTRUMENTATION_CLASS_FALLBACK)
+ expect(described_class.usage_activity_by_stage_plan(described_class.monthly_time_range_db_params)).to include(issues: Gitlab::Utils::UsageData::INSTRUMENTATION_CLASS_FALLBACK)
+
+ uncached_data = described_class.uncached_data
+ expect(uncached_data[:usage_activity_by_stage][:plan]).to include(issues: 3)
+ expect(uncached_data[:usage_activity_by_stage_monthly][:plan]).to include(issues: 2)
+ end
+ end
+
+ context 'when disabled' do
+ it 'does not merge the data from instrumentation classes' do
+ stub_feature_flags(usage_data_instrumentation: false)
+
+ for_defined_days_back do
+ user = create(:user)
+ project = create(:project, creator: user)
+ create(:issue, project: project, author: user)
+ create(:issue, project: project, author: User.support_bot)
+ end
+
+ expect(described_class.usage_activity_by_stage_plan({})).to include(issues: 3)
+ expect(described_class.usage_activity_by_stage_plan(described_class.monthly_time_range_db_params)).to include(issues: 2)
+ end
+ end
+ end
end
describe 'usage_activity_by_stage_release' do
@@ -466,17 +510,53 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
deployments: 2,
failed_deployments: 2,
releases: 2,
- successful_deployments: 2,
- releases_with_milestones: 2
+ successful_deployments: 2
)
expect(described_class.usage_activity_by_stage_release(described_class.monthly_time_range_db_params)).to include(
deployments: 1,
failed_deployments: 1,
releases: 1,
- successful_deployments: 1,
- releases_with_milestones: 1
+ successful_deployments: 1
)
end
+
+ context 'with usage_data_instrumentation feature flag' do
+ before do
+ for_defined_days_back do
+ user = create(:user)
+ create(:deployment, :failed, user: user)
+ release = create(:release, author: user)
+ create(:milestone, project: release.project, releases: [release])
+ create(:deployment, :success, user: user)
+ end
+ end
+
+ context 'when enabled' do
+ before do
+ stub_feature_flags(usage_data_instrumentation: true)
+ end
+
+ it 'merges data from instrumentation classes' do
+ expect(described_class.usage_activity_by_stage_release({})).to include(releases_with_milestones: Gitlab::Utils::UsageData::INSTRUMENTATION_CLASS_FALLBACK)
+ expect(described_class.usage_activity_by_stage_release(described_class.monthly_time_range_db_params)).to include(releases_with_milestones: Gitlab::Utils::UsageData::INSTRUMENTATION_CLASS_FALLBACK)
+
+ uncached_data = described_class.uncached_data
+ expect(uncached_data[:usage_activity_by_stage][:release]).to include(releases_with_milestones: 2)
+ expect(uncached_data[:usage_activity_by_stage_monthly][:release]).to include(releases_with_milestones: 1)
+ end
+ end
+
+ context 'when disabled' do
+ before do
+ stub_feature_flags(usage_data_instrumentation: false)
+ end
+
+ it 'does not merge data from instrumentation classes' do
+ expect(described_class.usage_activity_by_stage_release({})).to include(releases_with_milestones: 2)
+ expect(described_class.usage_activity_by_stage_release(described_class.monthly_time_range_db_params)).to include(releases_with_milestones: 1)
+ end
+ end
+ end
end
describe 'usage_activity_by_stage_verify' do
@@ -525,16 +605,16 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
subject { described_class.data }
it 'gathers usage data' do
- expect(subject.keys).to include(*UsageDataHelpers::USAGE_DATA_KEYS)
+ expect(subject.keys).to include(*UsageDataHelpers::USAGE_DATA_KEYS.map(&:to_s))
end
it 'gathers usage counts', :aggregate_failures do
count_data = subject[:counts]
-
expect(count_data[:boards]).to eq(1)
expect(count_data[:projects]).to eq(4)
- expect(count_data.keys).to include(*UsageDataHelpers::COUNTS_KEYS)
- expect(UsageDataHelpers::COUNTS_KEYS - count_data.keys).to be_empty
+ count_keys = UsageDataHelpers::COUNTS_KEYS.map(&:to_s)
+ expect(count_data.keys).to include(*count_keys)
+ expect(count_keys - count_data.keys).to be_empty
expect(count_data.values).to all(be_a_kind_of(Integer))
end
@@ -619,7 +699,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
external_diffs: { enabled: false },
lfs: { enabled: true, object_store: { enabled: false, direct_upload: true, background_upload: false, provider: "AWS" } },
uploads: { enabled: nil, object_store: { enabled: false, direct_upload: true, background_upload: false, provider: "AWS" } },
- packages: { enabled: true, object_store: { enabled: false, direct_upload: false, background_upload: true, provider: "AWS" } } }
+ packages: { enabled: true, object_store: { enabled: false, direct_upload: false, background_upload: true, provider: "AWS" } } }.with_indifferent_access
)
end
@@ -793,12 +873,37 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
subject { described_class.license_usage_data }
it 'gathers license data' do
- expect(subject[:uuid]).to eq(Gitlab::CurrentSettings.uuid)
expect(subject[:version]).to eq(Gitlab::VERSION)
expect(subject[:installation_type]).to eq('gitlab-development-kit')
- expect(subject[:active_user_count]).to eq(User.active.size)
expect(subject[:recorded_at]).to be_a(Time)
end
+
+ context 'with usage_data_instrumentation feature flag' do
+ context 'when enabled' do
+ it 'merges uuid and hostname data from instrumentation classes' do
+ stub_feature_flags(usage_data_instrumentation: true)
+
+ expect(subject[:uuid]).to eq(Gitlab::Utils::UsageData::INSTRUMENTATION_CLASS_FALLBACK)
+ expect(subject[:hostname]).to eq(Gitlab::Utils::UsageData::INSTRUMENTATION_CLASS_FALLBACK)
+ expect(subject[:active_user_count]).to eq(Gitlab::Utils::UsageData::INSTRUMENTATION_CLASS_FALLBACK)
+
+ uncached_data = described_class.data
+ expect(uncached_data[:uuid]).to eq(Gitlab::CurrentSettings.uuid)
+ expect(uncached_data[:hostname]).to eq(Gitlab.config.gitlab.host)
+ expect(uncached_data[:active_user_count]).to eq(User.active.size)
+ end
+ end
+
+ context 'when disabled' do
+ it 'does not merge uuid and hostname data from instrumentation classes' do
+ stub_feature_flags(usage_data_instrumentation: false)
+
+ expect(subject[:uuid]).to eq(Gitlab::CurrentSettings.uuid)
+ expect(subject[:hostname]).to eq(Gitlab.config.gitlab.host)
+ expect(subject[:active_user_count]).to eq(User.active.size)
+ end
+ end
+ end
end
context 'when not relying on database records' do
@@ -873,9 +978,9 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
expect(subject[:gitlab_pages][:enabled]).to eq(Gitlab.config.pages.enabled)
expect(subject[:gitlab_pages][:version]).to eq(Gitlab::Pages::VERSION)
expect(subject[:git][:version]).to eq(Gitlab::Git.version)
- expect(subject[:database][:adapter]).to eq(Gitlab::Database.main.adapter_name)
- expect(subject[:database][:version]).to eq(Gitlab::Database.main.version)
- expect(subject[:database][:pg_system_id]).to eq(Gitlab::Database.main.system_id)
+ expect(subject[:database][:adapter]).to eq(ApplicationRecord.database.adapter_name)
+ expect(subject[:database][:version]).to eq(ApplicationRecord.database.version)
+ expect(subject[:database][:pg_system_id]).to eq(ApplicationRecord.database.system_id)
expect(subject[:mail][:smtp_server]).to eq(ActionMailer::Base.smtp_settings[:address])
expect(subject[:gitaly][:version]).to be_present
expect(subject[:gitaly][:servers]).to be >= 1
@@ -1061,18 +1166,46 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
expect(subject[:settings][:gitaly_apdex]).to be_within(0.001).of(0.95)
end
- it 'reports collected data categories' do
- expected_value = %w[standard subscription operational optional]
+ context 'with usage_data_instrumentation feature flag' do
+ context 'when enabled' do
+ before do
+ stub_feature_flags(usage_data_instrumentation: true)
+ end
+
+ it 'reports collected data categories' do
+ expected_value = %w[standard subscription operational optional]
+
+ allow_next_instance_of(ServicePing::PermitDataCategoriesService) do |instance|
+ expect(instance).to receive(:execute).and_return(expected_value)
+ end
+
+ expect(described_class.data[:settings][:collected_data_categories]).to eq(expected_value)
+ end
- allow_next_instance_of(ServicePing::PermitDataCategoriesService) do |instance|
- expect(instance).to receive(:execute).and_return(expected_value)
+ it 'gathers service_ping_features_enabled' do
+ expect(described_class.data[:settings][:service_ping_features_enabled]).to eq(Gitlab::CurrentSettings.usage_ping_features_enabled)
+ end
end
- expect(subject[:settings][:collected_data_categories]).to eq(expected_value)
- end
+ context 'when disabled' do
+ before do
+ stub_feature_flags(usage_data_instrumentation: false)
+ end
+
+ it 'reports collected data categories' do
+ expected_value = %w[standard subscription operational optional]
- it 'gathers service_ping_features_enabled' do
- expect(subject[:settings][:service_ping_features_enabled]).to eq(Gitlab::CurrentSettings.usage_ping_features_enabled)
+ allow_next_instance_of(ServicePing::PermitDataCategoriesService) do |instance|
+ expect(instance).to receive(:execute).and_return(expected_value)
+ end
+
+ expect(subject[:settings][:collected_data_categories]).to eq(expected_value)
+ end
+
+ it 'gathers service_ping_features_enabled' do
+ expect(subject[:settings][:service_ping_features_enabled]).to eq(Gitlab::CurrentSettings.usage_ping_features_enabled)
+ end
+ end
end
it 'gathers user_cap_feature_enabled' do
diff --git a/spec/lib/gitlab/utils/usage_data_spec.rb b/spec/lib/gitlab/utils/usage_data_spec.rb
index 1d01d5c7e6a..e721b28ac29 100644
--- a/spec/lib/gitlab/utils/usage_data_spec.rb
+++ b/spec/lib/gitlab/utils/usage_data_spec.rb
@@ -8,8 +8,26 @@ RSpec.describe Gitlab::Utils::UsageData do
describe '#add_metric' do
let(:metric) { 'UuidMetric'}
- it 'computes the metric value for given metric' do
- expect(described_class.add_metric(metric)).to eq(Gitlab::CurrentSettings.uuid)
+ context 'with usage_data_instrumentation feature flag' do
+ context 'when enabled' do
+ before do
+ stub_feature_flags(usage_data_instrumentation: true)
+ end
+
+ it 'returns -100 value to be overriden' do
+ expect(described_class.add_metric(metric)).to eq(-100)
+ end
+ end
+
+ context 'when disabled' do
+ before do
+ stub_feature_flags(usage_data_instrumentation: false)
+ end
+
+ it 'computes the metric value for given metric' do
+ expect(described_class.add_metric(metric)).to eq(Gitlab::CurrentSettings.uuid)
+ end
+ end
end
end
@@ -52,7 +70,7 @@ RSpec.describe Gitlab::Utils::UsageData do
let(:relation) { double(:relation, connection: double(:connection)) }
before do
- allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false) # rubocop: disable Database/MultipleDatabases
+ allow(relation.connection).to receive(:transaction_open?).and_return(false)
end
it 'delegates counting to counter class instance' do
@@ -104,7 +122,7 @@ RSpec.describe Gitlab::Utils::UsageData do
let(:ci_builds_estimated_cardinality) { 2.0809220082170614 }
before do
- allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false) # rubocop: disable Database/MultipleDatabases
+ allow(model.connection).to receive(:transaction_open?).and_return(false)
end
context 'different counting parameters' do
diff --git a/spec/lib/gitlab/webpack/file_loader_spec.rb b/spec/lib/gitlab/webpack/file_loader_spec.rb
new file mode 100644
index 00000000000..34d00b9f106
--- /dev/null
+++ b/spec/lib/gitlab/webpack/file_loader_spec.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'support/helpers/file_read_helpers'
+require 'support/webmock'
+
+RSpec.describe Gitlab::Webpack::FileLoader do
+ include FileReadHelpers
+ include WebMock::API
+
+ let(:error_file_path) { "error.yml" }
+ let(:file_path) { "my_test_file.yml" }
+ let(:file_contents) do
+ <<-EOF
+ - hello
+ - world
+ - test
+ EOF
+ end
+
+ before do
+ allow(Gitlab.config.webpack.dev_server).to receive_messages(host: 'hostname', port: 2000, https: false)
+ allow(Gitlab.config.webpack).to receive(:public_path).and_return('public_path')
+ allow(Gitlab.config.webpack).to receive(:output_dir).and_return('webpack_output')
+ end
+
+ context "with dev server enabled" do
+ before do
+ allow(Gitlab.config.webpack.dev_server).to receive(:enabled).and_return(true)
+
+ stub_request(:get, "http://hostname:2000/public_path/not_found").to_return(status: 404)
+ stub_request(:get, "http://hostname:2000/public_path/#{file_path}").to_return(body: file_contents, status: 200)
+ stub_request(:get, "http://hostname:2000/public_path/#{error_file_path}").to_raise(StandardError)
+ end
+
+ it "returns content when respondes succesfully" do
+ expect(Gitlab::Webpack::FileLoader.load(file_path)).to be(file_contents)
+ end
+
+ it "raises error when 404" do
+ expect { Gitlab::Webpack::FileLoader.load("not_found") }.to raise_error("HTTP error 404")
+ end
+
+ it "raises error when errors out" do
+ expect { Gitlab::Webpack::FileLoader.load(error_file_path) }.to raise_error(Gitlab::Webpack::FileLoader::DevServerLoadError)
+ end
+ end
+
+ context "with dev server enabled and https" do
+ before do
+ allow(Gitlab.config.webpack.dev_server).to receive(:enabled).and_return(true)
+ allow(Gitlab.config.webpack.dev_server).to receive(:https).and_return(true)
+
+ stub_request(:get, "https://hostname:2000/public_path/#{error_file_path}").to_raise(EOFError)
+ end
+
+ it "raises error if catches SSLError" do
+ expect { Gitlab::Webpack::FileLoader.load(error_file_path) }.to raise_error(Gitlab::Webpack::FileLoader::DevServerSSLError)
+ end
+ end
+
+ context "with dev server disabled" do
+ before do
+ allow(Gitlab.config.webpack.dev_server).to receive(:enabled).and_return(false)
+ stub_file_read(::Rails.root.join("webpack_output/#{file_path}"), content: file_contents)
+ stub_file_read(::Rails.root.join("webpack_output/#{error_file_path}"), error: Errno::ENOENT)
+ end
+
+ describe ".load" do
+ it "returns file content from file path" do
+ expect(Gitlab::Webpack::FileLoader.load(file_path)).to be(file_contents)
+ end
+
+ it "throws error if file cannot be read" do
+ expect { Gitlab::Webpack::FileLoader.load(error_file_path) }.to raise_error(Gitlab::Webpack::FileLoader::StaticLoadError)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/webpack/graphql_known_operations_spec.rb b/spec/lib/gitlab/webpack/graphql_known_operations_spec.rb
new file mode 100644
index 00000000000..89cade82fe6
--- /dev/null
+++ b/spec/lib/gitlab/webpack/graphql_known_operations_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Webpack::GraphqlKnownOperations do
+ let(:content) do
+ <<-EOF
+ - hello
+ - world
+ - test
+ EOF
+ end
+
+ around do |example|
+ described_class.clear_memoization!
+
+ example.run
+
+ described_class.clear_memoization!
+ end
+
+ describe ".load" do
+ context "when file loader returns" do
+ before do
+ allow(::Gitlab::Webpack::FileLoader).to receive(:load).with("graphql_known_operations.yml").and_return(content)
+ end
+
+ it "returns memoized value" do
+ expect(::Gitlab::Webpack::FileLoader).to receive(:load).once
+
+ 2.times { ::Gitlab::Webpack::GraphqlKnownOperations.load }
+
+ expect(::Gitlab::Webpack::GraphqlKnownOperations.load).to eq(%w(hello world test))
+ end
+ end
+
+ context "when file loader errors" do
+ before do
+ allow(::Gitlab::Webpack::FileLoader).to receive(:load).and_raise(StandardError.new("test"))
+ end
+
+ it "returns empty array" do
+ expect(::Gitlab::Webpack::GraphqlKnownOperations.load).to eq([])
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index 8ba56af561d..3bab9aec454 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -512,6 +512,24 @@ RSpec.describe Gitlab::Workhorse do
end
end
+ describe '.send_dependency' do
+ let(:headers) { { Accept: 'foo', Authorization: 'Bearer asdf1234' } }
+ let(:url) { 'https://foo.bar.com/baz' }
+
+ subject { described_class.send_dependency(headers, url) }
+
+ it 'sets the header correctly', :aggregate_failures do
+ key, command, params = decode_workhorse_header(subject)
+
+ expect(key).to eq("Gitlab-Workhorse-Send-Data")
+ expect(command).to eq("send-dependency")
+ expect(params).to eq({
+ 'Header' => headers,
+ 'Url' => url
+ }.deep_stringify_keys)
+ end
+ end
+
describe '.send_git_snapshot' do
let(:url) { 'http://example.com' }
diff --git a/spec/lib/gitlab/x509/certificate_spec.rb b/spec/lib/gitlab/x509/certificate_spec.rb
index a5b192dd051..2dc30cc871d 100644
--- a/spec/lib/gitlab/x509/certificate_spec.rb
+++ b/spec/lib/gitlab/x509/certificate_spec.rb
@@ -5,6 +5,9 @@ require 'spec_helper'
RSpec.describe Gitlab::X509::Certificate do
include SmimeHelper
+ let(:sample_ca_certs_path) { Rails.root.join('spec/fixtures/clusters').to_s }
+ let(:sample_cert) { Rails.root.join('spec/fixtures/x509_certificate.crt').to_s }
+
# cert generation is an expensive operation and they are used read-only,
# so we share them as instance variables in all tests
before :context do
@@ -13,6 +16,16 @@ RSpec.describe Gitlab::X509::Certificate do
@cert = generate_cert(signer_ca: @intermediate_ca)
end
+ before do
+ stub_const("OpenSSL::X509::DEFAULT_CERT_DIR", sample_ca_certs_path)
+ stub_const("OpenSSL::X509::DEFAULT_CERT_FILE", sample_cert)
+ described_class.reset_ca_certs_bundle
+ end
+
+ after(:context) do
+ described_class.reset_ca_certs_bundle
+ end
+
describe 'testing environment setup' do
describe 'generate_root' do
subject { @root_ca }
@@ -103,6 +116,43 @@ RSpec.describe Gitlab::X509::Certificate do
end
end
+ describe '.ca_certs_paths' do
+ it 'returns all files specified by OpenSSL defaults' do
+ cert_paths = Dir["#{OpenSSL::X509::DEFAULT_CERT_DIR}/*"]
+
+ expect(described_class.ca_certs_paths).to match_array(cert_paths + [sample_cert])
+ end
+ end
+
+ describe '.ca_certs_bundle' do
+ it 'skips certificates if OpenSSLError is raised and report it' do
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_and_raise_for_dev_exception)
+ .with(
+ a_kind_of(OpenSSL::X509::CertificateError),
+ cert_file: a_kind_of(String)).at_least(:once)
+
+ expect(OpenSSL::X509::Certificate)
+ .to receive(:new)
+ .and_raise(OpenSSL::X509::CertificateError).at_least(:once)
+
+ expect(described_class.ca_certs_bundle).to be_a(String)
+ end
+
+ it 'returns a list certificates as strings' do
+ expect(described_class.ca_certs_bundle).to be_a(String)
+ end
+ end
+
+ describe '.load_ca_certs_bundle' do
+ it 'loads a PEM-encoded certificate bundle into an OpenSSL::X509::Certificate array' do
+ ca_certs_string = described_class.ca_certs_bundle
+ ca_certs = described_class.load_ca_certs_bundle(ca_certs_string)
+
+ expect(ca_certs).to all(be_an(OpenSSL::X509::Certificate))
+ end
+ end
+
def common_cert_tests(parsed_cert, cert, signer_ca, with_ca_certs: nil)
expect(parsed_cert.cert).to be_a(OpenSSL::X509::Certificate)
expect(parsed_cert.cert.subject).to eq(cert[:cert].subject)
diff --git a/spec/lib/gitlab/x509/signature_spec.rb b/spec/lib/gitlab/x509/signature_spec.rb
index 7ba15faf910..0e34d5393d6 100644
--- a/spec/lib/gitlab/x509/signature_spec.rb
+++ b/spec/lib/gitlab/x509/signature_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Gitlab::X509::Signature do
end
shared_examples "a verified signature" do
- let_it_be(:user) { create(:user, email: X509Helpers::User1.certificate_email) }
+ let!(:user) { create(:user, email: X509Helpers::User1.certificate_email) }
subject(:signature) do
described_class.new(
@@ -30,10 +30,12 @@ RSpec.describe Gitlab::X509::Signature do
expect(signature.verification_status).to eq(:verified)
end
- it "returns an unverified signature if the email matches but isn't confirmed" do
- user.update!(confirmed_at: nil)
+ context "if the email matches but isn't confirmed" do
+ let!(:user) { create(:user, :unconfirmed, email: X509Helpers::User1.certificate_email) }
- expect(signature.verification_status).to eq(:unverified)
+ it "returns an unverified signature" do
+ expect(signature.verification_status).to eq(:unverified)
+ end
end
it 'returns an unverified signature if email does not match' do
@@ -297,7 +299,7 @@ RSpec.describe Gitlab::X509::Signature do
end
context 'verified signature' do
- let_it_be(:user) { create(:user, email: X509Helpers::User1.certificate_email) }
+ let_it_be(:user) { create(:user, :unconfirmed, email: X509Helpers::User1.certificate_email) }
subject(:signature) do
described_class.new(
@@ -316,52 +318,56 @@ RSpec.describe Gitlab::X509::Signature do
allow(OpenSSL::X509::Store).to receive(:new).and_return(store)
end
- it 'returns a verified signature if email does match' do
- expect(signature.x509_certificate).to have_attributes(certificate_attributes)
- expect(signature.x509_certificate.x509_issuer).to have_attributes(issuer_attributes)
- expect(signature.verified_signature).to be_truthy
- expect(signature.verification_status).to eq(:verified)
- end
+ context 'when user email is confirmed' do
+ before_all do
+ user.confirm
+ end
- it "returns an unverified signature if the email matches but isn't confirmed" do
- user.update!(confirmed_at: nil)
+ it 'returns a verified signature if email does match', :ggregate_failures do
+ expect(signature.x509_certificate).to have_attributes(certificate_attributes)
+ expect(signature.x509_certificate.x509_issuer).to have_attributes(issuer_attributes)
+ expect(signature.verified_signature).to be_truthy
+ expect(signature.verification_status).to eq(:verified)
+ end
- expect(signature.verification_status).to eq(:unverified)
- end
+ it 'returns an unverified signature if email does not match', :aggregate_failures do
+ signature = described_class.new(
+ X509Helpers::User1.signed_tag_signature,
+ X509Helpers::User1.signed_tag_base_data,
+ "gitlab@example.com",
+ X509Helpers::User1.signed_commit_time
+ )
+
+ expect(signature.x509_certificate).to have_attributes(certificate_attributes)
+ expect(signature.x509_certificate.x509_issuer).to have_attributes(issuer_attributes)
+ expect(signature.verified_signature).to be_truthy
+ expect(signature.verification_status).to eq(:unverified)
+ end
- it 'returns an unverified signature if email does not match' do
- signature = described_class.new(
- X509Helpers::User1.signed_tag_signature,
- X509Helpers::User1.signed_tag_base_data,
- "gitlab@example.com",
- X509Helpers::User1.signed_commit_time
- )
+ it 'returns an unverified signature if email does match and time is wrong', :aggregate_failures do
+ signature = described_class.new(
+ X509Helpers::User1.signed_tag_signature,
+ X509Helpers::User1.signed_tag_base_data,
+ X509Helpers::User1.certificate_email,
+ Time.new(2020, 2, 22)
+ )
+
+ expect(signature.x509_certificate).to have_attributes(certificate_attributes)
+ expect(signature.x509_certificate.x509_issuer).to have_attributes(issuer_attributes)
+ expect(signature.verified_signature).to be_falsey
+ expect(signature.verification_status).to eq(:unverified)
+ end
- expect(signature.x509_certificate).to have_attributes(certificate_attributes)
- expect(signature.x509_certificate.x509_issuer).to have_attributes(issuer_attributes)
- expect(signature.verified_signature).to be_truthy
- expect(signature.verification_status).to eq(:unverified)
- end
+ it 'returns an unverified signature if certificate is revoked' do
+ expect(signature.verification_status).to eq(:verified)
- it 'returns an unverified signature if email does match and time is wrong' do
- signature = described_class.new(
- X509Helpers::User1.signed_tag_signature,
- X509Helpers::User1.signed_tag_base_data,
- X509Helpers::User1.certificate_email,
- Time.new(2020, 2, 22)
- )
+ signature.x509_certificate.revoked!
- expect(signature.x509_certificate).to have_attributes(certificate_attributes)
- expect(signature.x509_certificate.x509_issuer).to have_attributes(issuer_attributes)
- expect(signature.verified_signature).to be_falsey
- expect(signature.verification_status).to eq(:unverified)
+ expect(signature.verification_status).to eq(:unverified)
+ end
end
- it 'returns an unverified signature if certificate is revoked' do
- expect(signature.verification_status).to eq(:verified)
-
- signature.x509_certificate.revoked!
-
+ it 'returns an unverified signature if the email matches but is not confirmed' do
expect(signature.verification_status).to eq(:unverified)
end
end
diff --git a/spec/lib/gitlab/zentao/client_spec.rb b/spec/lib/gitlab/zentao/client_spec.rb
index e3a335c1e89..86b310fe417 100644
--- a/spec/lib/gitlab/zentao/client_spec.rb
+++ b/spec/lib/gitlab/zentao/client_spec.rb
@@ -6,7 +6,23 @@ RSpec.describe Gitlab::Zentao::Client do
subject(:integration) { described_class.new(zentao_integration) }
let(:zentao_integration) { create(:zentao_integration) }
- let(:mock_get_products_url) { integration.send(:url, "products/#{zentao_integration.zentao_product_xid}") }
+
+ def mock_get_products_url
+ integration.send(:url, "products/#{zentao_integration.zentao_product_xid}")
+ end
+
+ def mock_fetch_issue_url(issue_id)
+ integration.send(:url, "issues/#{issue_id}")
+ end
+
+ let(:mock_headers) do
+ {
+ headers: {
+ 'Content-Type' => 'application/json',
+ 'Token' => zentao_integration.api_token
+ }
+ }
+ end
describe '#new' do
context 'if integration is nil' do
@@ -25,15 +41,6 @@ RSpec.describe Gitlab::Zentao::Client do
end
describe '#fetch_product' do
- let(:mock_headers) do
- {
- headers: {
- 'Content-Type' => 'application/json',
- 'Token' => zentao_integration.api_token
- }
- }
- end
-
context 'with valid product' do
let(:mock_response) { { 'id' => zentao_integration.zentao_product_xid } }
@@ -54,7 +61,9 @@ RSpec.describe Gitlab::Zentao::Client do
end
it 'fetches the empty product' do
- expect(integration.fetch_product(zentao_integration.zentao_product_xid)).to eq({})
+ expect do
+ integration.fetch_product(zentao_integration.zentao_product_xid)
+ end.to raise_error(Gitlab::Zentao::Client::Error, 'request error')
end
end
@@ -65,21 +74,14 @@ RSpec.describe Gitlab::Zentao::Client do
end
it 'fetches the empty product' do
- expect(integration.fetch_product(zentao_integration.zentao_product_xid)).to eq({})
+ expect do
+ integration.fetch_product(zentao_integration.zentao_product_xid)
+ end.to raise_error(Gitlab::Zentao::Client::Error, 'invalid response format')
end
end
end
describe '#ping' do
- let(:mock_headers) do
- {
- headers: {
- 'Content-Type' => 'application/json',
- 'Token' => zentao_integration.api_token
- }
- }
- end
-
context 'with valid resource' do
before do
WebMock.stub_request(:get, mock_get_products_url)
@@ -102,4 +104,30 @@ RSpec.describe Gitlab::Zentao::Client do
end
end
end
+
+ describe '#fetch_issue' do
+ context 'with invalid id' do
+ let(:invalid_ids) { ['story', 'story-', '-', '123', ''] }
+
+ it 'returns empty object' do
+ invalid_ids.each do |id|
+ expect { integration.fetch_issue(id) }
+ .to raise_error(Gitlab::Zentao::Client::Error, 'invalid issue id')
+ end
+ end
+ end
+
+ context 'with valid id' do
+ let(:valid_ids) { %w[story-1 bug-23] }
+
+ it 'fetches current issue' do
+ valid_ids.each do |id|
+ WebMock.stub_request(:get, mock_fetch_issue_url(id))
+ .with(mock_headers).to_return(status: 200, body: { issue: { id: id } }.to_json)
+
+ expect(integration.fetch_issue(id).dig('issue', 'id')).to eq id
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/zentao/query_spec.rb b/spec/lib/gitlab/zentao/query_spec.rb
new file mode 100644
index 00000000000..f7495e640c3
--- /dev/null
+++ b/spec/lib/gitlab/zentao/query_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Zentao::Query do
+ let(:zentao_integration) { create(:zentao_integration) }
+ let(:params) { {} }
+
+ subject(:query) { described_class.new(zentao_integration, ActionController::Parameters.new(params)) }
+
+ describe '#issues' do
+ let(:response) { { 'page' => 1, 'total' => 0, 'limit' => 20, 'issues' => [] } }
+
+ def expect_query_option_include(expected_params)
+ expect_next_instance_of(Gitlab::Zentao::Client) do |client|
+ expect(client).to receive(:fetch_issues)
+ .with(hash_including(expected_params))
+ .and_return(response)
+ end
+
+ query.issues
+ end
+
+ context 'when params are empty' do
+ it 'fills default params' do
+ expect_query_option_include(status: 'opened', order: 'lastEditedDate_desc', labels: '')
+ end
+ end
+
+ context 'when params contain valid options' do
+ let(:params) { { state: 'closed', sort: 'created_asc', labels: %w[Bugs Features] } }
+
+ it 'fills params with standard of ZenTao' do
+ expect_query_option_include(status: 'closed', order: 'openedDate_asc', labels: 'Bugs,Features')
+ end
+ end
+
+ context 'when params contain invalid options' do
+ let(:params) { { state: 'xxx', sort: 'xxx', labels: %w[xxx] } }
+
+ it 'fills default params with standard of ZenTao' do
+ expect_query_option_include(status: 'opened', order: 'lastEditedDate_desc', labels: 'xxx')
+ end
+ end
+ end
+
+ describe '#issue' do
+ let(:response) { { 'issue' => { 'id' => 'story-1' } } }
+
+ before do
+ expect_next_instance_of(Gitlab::Zentao::Client) do |client|
+ expect(client).to receive(:fetch_issue)
+ .and_return(response)
+ end
+ end
+
+ it 'returns issue object by client' do
+ expect(query.issue).to include('id' => 'story-1')
+ end
+ end
+end
diff --git a/spec/lib/marginalia_spec.rb b/spec/lib/marginalia_spec.rb
index 3f39d969dbd..53048ae2e6b 100644
--- a/spec/lib/marginalia_spec.rb
+++ b/spec/lib/marginalia_spec.rb
@@ -59,14 +59,14 @@ RSpec.describe 'Marginalia spec' do
"application" => "test",
"endpoint_id" => "MarginaliaTestController#first_user",
"correlation_id" => correlation_id,
- "db_config_name" => "ci"
+ "db_config_name" => ENV['GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci'] == 'main' ? 'main' : 'ci'
}
end
- before do |example|
+ before do
skip_if_multiple_databases_not_setup
- allow(User).to receive(:connection) { Ci::CiDatabaseRecord.connection }
+ allow(User).to receive(:connection) { Ci::ApplicationRecord.connection }
end
it 'generates a query that includes the component and value' do
diff --git a/spec/lib/object_storage/config_spec.rb b/spec/lib/object_storage/config_spec.rb
index 21b8a44b3d6..9a0e83bfd5e 100644
--- a/spec/lib/object_storage/config_spec.rb
+++ b/spec/lib/object_storage/config_spec.rb
@@ -36,46 +36,6 @@ RSpec.describe ObjectStorage::Config do
subject { described_class.new(raw_config.as_json) }
- describe '#load_provider' do
- before do
- subject.load_provider
- end
-
- context 'with AWS' do
- it 'registers AWS as a provider' do
- expect(Fog.providers.keys).to include(:aws)
- end
- end
-
- context 'with Google' do
- let(:credentials) do
- {
- provider: 'Google',
- google_storage_access_key_id: 'GOOGLE_ACCESS_KEY_ID',
- google_storage_secret_access_key: 'GOOGLE_SECRET_ACCESS_KEY'
- }
- end
-
- it 'registers Google as a provider' do
- expect(Fog.providers.keys).to include(:google)
- end
- end
-
- context 'with Azure' do
- let(:credentials) do
- {
- provider: 'AzureRM',
- azure_storage_account_name: 'azuretest',
- azure_storage_access_key: 'ABCD1234'
- }
- end
-
- it 'registers AzureRM as a provider' do
- expect(Fog.providers.keys).to include(:azurerm)
- end
- end
- end
-
describe '#credentials' do
it { expect(subject.credentials).to eq(credentials) }
end
diff --git a/spec/lib/object_storage/direct_upload_spec.rb b/spec/lib/object_storage/direct_upload_spec.rb
index 006f4f603b6..1629aec89f5 100644
--- a/spec/lib/object_storage/direct_upload_spec.rb
+++ b/spec/lib/object_storage/direct_upload_spec.rb
@@ -201,10 +201,6 @@ RSpec.describe ObjectStorage::DirectUpload do
end
shared_examples 'a valid AzureRM upload' do
- before do
- require 'fog/azurerm'
- end
-
it_behaves_like 'a valid upload'
it 'enables the Workhorse client' do
diff --git a/spec/lib/security/ci_configuration/sast_iac_build_action_spec.rb b/spec/lib/security/ci_configuration/sast_iac_build_action_spec.rb
new file mode 100644
index 00000000000..ecd1602dd9e
--- /dev/null
+++ b/spec/lib/security/ci_configuration/sast_iac_build_action_spec.rb
@@ -0,0 +1,163 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Security::CiConfiguration::SastIacBuildAction do
+ subject(:result) { described_class.new(auto_devops_enabled, gitlab_ci_content).generate }
+
+ let(:params) { {} }
+
+ context 'with existing .gitlab-ci.yml' do
+ let(:auto_devops_enabled) { false }
+
+ context 'sast iac has not been included' do
+ let(:expected_yml) do
+ <<-CI_YML.strip_heredoc
+ # You can override the included template(s) by including variable overrides
+ # SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
+ # Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
+ # Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
+ # Note that environment variables can be set in several places
+ # See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
+ stages:
+ - test
+ - security
+ variables:
+ RANDOM: make sure this persists
+ include:
+ - template: existing.yml
+ - template: Security/SAST-IaC.latest.gitlab-ci.yml
+ CI_YML
+ end
+
+ context 'template includes are an array' do
+ let(:gitlab_ci_content) do
+ { "stages" => %w(test security),
+ "variables" => { "RANDOM" => "make sure this persists" },
+ "include" => [{ "template" => "existing.yml" }] }
+ end
+
+ it 'generates the correct YML' do
+ expect(result[:action]).to eq('update')
+ expect(result[:content]).to eq(expected_yml)
+ end
+ end
+
+ context 'template include is not an array' do
+ let(:gitlab_ci_content) do
+ { "stages" => %w(test security),
+ "variables" => { "RANDOM" => "make sure this persists" },
+ "include" => { "template" => "existing.yml" } }
+ end
+
+ it 'generates the correct YML' do
+ expect(result[:action]).to eq('update')
+ expect(result[:content]).to eq(expected_yml)
+ end
+ end
+ end
+
+ context 'secret_detection has been included' do
+ let(:expected_yml) do
+ <<-CI_YML.strip_heredoc
+ # You can override the included template(s) by including variable overrides
+ # SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
+ # Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
+ # Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
+ # Note that environment variables can be set in several places
+ # See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
+ stages:
+ - test
+ variables:
+ RANDOM: make sure this persists
+ include:
+ - template: Security/SAST-IaC.latest.gitlab-ci.yml
+ CI_YML
+ end
+
+ context 'secret_detection template include are an array' do
+ let(:gitlab_ci_content) do
+ { "stages" => %w(test),
+ "variables" => { "RANDOM" => "make sure this persists" },
+ "include" => [{ "template" => "Security/SAST-IaC.latest.gitlab-ci.yml" }] }
+ end
+
+ it 'generates the correct YML' do
+ expect(result[:action]).to eq('update')
+ expect(result[:content]).to eq(expected_yml)
+ end
+ end
+
+ context 'secret_detection template include is not an array' do
+ let(:gitlab_ci_content) do
+ { "stages" => %w(test),
+ "variables" => { "RANDOM" => "make sure this persists" },
+ "include" => { "template" => "Security/SAST-IaC.latest.gitlab-ci.yml" } }
+ end
+
+ it 'generates the correct YML' do
+ expect(result[:action]).to eq('update')
+ expect(result[:content]).to eq(expected_yml)
+ end
+ end
+ end
+ end
+
+ context 'with no .gitlab-ci.yml' do
+ let(:gitlab_ci_content) { nil }
+
+ context 'autodevops disabled' do
+ let(:auto_devops_enabled) { false }
+ let(:expected_yml) do
+ <<-CI_YML.strip_heredoc
+ # You can override the included template(s) by including variable overrides
+ # SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
+ # Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
+ # Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
+ # Note that environment variables can be set in several places
+ # See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
+ include:
+ - template: Security/SAST-IaC.latest.gitlab-ci.yml
+ CI_YML
+ end
+
+ it 'generates the correct YML' do
+ expect(result[:action]).to eq('create')
+ expect(result[:content]).to eq(expected_yml)
+ end
+ end
+
+ context 'with autodevops enabled' do
+ let(:auto_devops_enabled) { true }
+ let(:expected_yml) do
+ <<-CI_YML.strip_heredoc
+ # You can override the included template(s) by including variable overrides
+ # SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
+ # Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
+ # Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
+ # Note that environment variables can be set in several places
+ # See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
+ include:
+ - template: Auto-DevOps.gitlab-ci.yml
+ CI_YML
+ end
+
+ before do
+ allow_next_instance_of(described_class) do |sast_iac_build_actions|
+ allow(sast_iac_build_actions).to receive(:auto_devops_stages).and_return(fast_auto_devops_stages)
+ end
+ end
+
+ it 'generates the correct YML' do
+ expect(result[:action]).to eq('create')
+ expect(result[:content]).to eq(expected_yml)
+ end
+ 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
+end
diff --git a/spec/lib/sidebars/groups/menus/invite_team_members_menu_spec.rb b/spec/lib/sidebars/groups/menus/invite_team_members_menu_spec.rb
new file mode 100644
index 00000000000..a79e5182f45
--- /dev/null
+++ b/spec/lib/sidebars/groups/menus/invite_team_members_menu_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Groups::Menus::InviteTeamMembersMenu do
+ let_it_be(:owner) { create(:user) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:group) do
+ build(:group).tap do |g|
+ g.add_owner(owner)
+ end
+ end
+
+ let(:context) { Sidebars::Groups::Context.new(current_user: owner, container: group) }
+
+ subject(:invite_menu) { described_class.new(context) }
+
+ context 'when the group is viewed by an owner of the group' do
+ describe '#render?' do
+ it 'renders the Invite team members link' do
+ expect(invite_menu.render?).to eq(true)
+ end
+
+ context 'when the group already has at least 2 members' do
+ before do
+ group.add_guest(guest)
+ end
+
+ it 'does not render the link' do
+ expect(invite_menu.render?).to eq(false)
+ end
+ end
+ end
+
+ describe '#title' do
+ it 'displays the correct Invite team members text for the link in the side nav' do
+ expect(invite_menu.title).to eq('Invite members')
+ end
+ end
+ end
+
+ context 'when the group is viewed by a guest user without admin permissions' do
+ let(:context) { Sidebars::Groups::Context.new(current_user: guest, container: group) }
+
+ before do
+ group.add_guest(guest)
+ end
+
+ describe '#render?' do
+ it 'does not render the link' do
+ expect(subject.render?).to eq(false)
+ end
+ end
+ end
+end
diff --git a/spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb b/spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb
index 5ebd67462f8..e954d7a44ba 100644
--- a/spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb
+++ b/spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb
@@ -137,16 +137,27 @@ RSpec.describe Sidebars::Groups::Menus::PackagesRegistriesMenu do
stub_config(dependency_proxy: { enabled: dependency_enabled })
end
- context 'when config dependency_proxy is enabled' do
- let(:dependency_enabled) { true }
+ context 'when user can read dependency proxy' do
+ context 'when config dependency_proxy is enabled' do
+ let(:dependency_enabled) { true }
- it 'the menu item is added to list of menu items' do
- is_expected.not_to be_nil
+ it 'the menu item is added to list of menu items' do
+ is_expected.not_to be_nil
+ end
+ end
+
+ context 'when config dependency_proxy is not enabled' do
+ let(:dependency_enabled) { false }
+
+ it 'the menu item is not added to list of menu items' do
+ is_expected.to be_nil
+ end
end
end
- context 'when config dependency_proxy is not enabled' do
- let(:dependency_enabled) { false }
+ context 'when user cannot read dependency proxy' do
+ let(:user) { nil }
+ let(:dependency_enabled) { true }
it 'the menu item is not added to list of menu items' do
is_expected.to be_nil
diff --git a/spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb b/spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb
index 2415598da9c..55281171634 100644
--- a/spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb
@@ -51,6 +51,16 @@ RSpec.describe Sidebars::Projects::Menus::InfrastructureMenu do
it 'menu link points to Terraform page' do
expect(subject.link).to eq find_menu_item(:terraform).link
end
+
+ context 'when Terraform menu is not visible' do
+ before do
+ subject.renderable_items.delete(find_menu_item(:terraform))
+ end
+
+ it 'menu link points to Google Cloud page' do
+ expect(subject.link).to eq find_menu_item(:google_cloud).link
+ end
+ end
end
end
@@ -89,5 +99,11 @@ RSpec.describe Sidebars::Projects::Menus::InfrastructureMenu do
it_behaves_like 'access rights checks'
end
+
+ describe 'Google Cloud' do
+ let(:item_id) { :google_cloud }
+
+ it_behaves_like 'access rights checks'
+ end
end
end
diff --git a/spec/lib/sidebars/projects/menus/invite_team_members_menu_spec.rb b/spec/lib/sidebars/projects/menus/invite_team_members_menu_spec.rb
new file mode 100644
index 00000000000..df9b260d211
--- /dev/null
+++ b/spec/lib/sidebars/projects/menus/invite_team_members_menu_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Projects::Menus::InviteTeamMembersMenu do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:guest) { create(:user) }
+
+ let(:context) { Sidebars::Projects::Context.new(current_user: owner, container: project) }
+
+ subject(:invite_menu) { described_class.new(context) }
+
+ context 'when the project is viewed by an owner of the group' do
+ let(:owner) { project.owner }
+
+ describe '#render?' do
+ it 'renders the Invite team members link' do
+ expect(invite_menu.render?).to eq(true)
+ end
+
+ context 'when the project already has at least 2 members' do
+ before do
+ project.add_guest(guest)
+ end
+
+ it 'does not render the link' do
+ expect(invite_menu.render?).to eq(false)
+ end
+ end
+ end
+
+ describe '#title' do
+ it 'displays the correct Invite team members text for the link in the side nav' do
+ expect(invite_menu.title).to eq('Invite members')
+ end
+ end
+ end
+
+ context 'when the project is viewed by a guest user without admin permissions' do
+ let(:context) { Sidebars::Projects::Context.new(current_user: guest, container: project) }
+
+ before do
+ project.add_guest(guest)
+ end
+
+ describe '#render?' do
+ it 'does not render' do
+ expect(invite_menu.render?).to eq(false)
+ end
+ end
+ end
+end
diff --git a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
index 3079c781d73..1e5d41dfec4 100644
--- a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
@@ -162,24 +162,10 @@ RSpec.describe Sidebars::Projects::Menus::SettingsMenu do
describe 'Usage Quotas' do
let(:item_id) { :usage_quotas }
- describe 'with project_storage_ui feature flag enabled' do
- before do
- stub_feature_flags(project_storage_ui: true)
- end
-
- specify { is_expected.not_to be_nil }
-
- describe 'when the user does not have access' do
- let(:user) { nil }
-
- specify { is_expected.to be_nil }
- end
- end
+ specify { is_expected.not_to be_nil }
- describe 'with project_storage_ui feature flag disabled' do
- before do
- stub_feature_flags(project_storage_ui: false)
- end
+ describe 'when the user does not have access' do
+ let(:user) { nil }
specify { is_expected.to be_nil }
end
diff --git a/spec/lib/sidebars/projects/menus/zentao_menu_spec.rb b/spec/lib/sidebars/projects/menus/zentao_menu_spec.rb
new file mode 100644
index 00000000000..f0bce6b7ea5
--- /dev/null
+++ b/spec/lib/sidebars/projects/menus/zentao_menu_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Projects::Menus::ZentaoMenu do
+ it_behaves_like 'ZenTao menu with CE version'
+end
diff --git a/spec/lib/system_check/incoming_email_check_spec.rb b/spec/lib/system_check/incoming_email_check_spec.rb
index 710702b93fc..5d93b810045 100644
--- a/spec/lib/system_check/incoming_email_check_spec.rb
+++ b/spec/lib/system_check/incoming_email_check_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe SystemCheck::IncomingEmailCheck do
it 'runs IMAP and mailroom checks' do
expect(SystemCheck).to receive(:run).with('Reply by email', [
SystemCheck::IncomingEmail::ImapAuthenticationCheck,
- SystemCheck::IncomingEmail::InitdConfiguredCheck,
+ SystemCheck::IncomingEmail::MailRoomEnabledCheck,
SystemCheck::IncomingEmail::MailRoomRunningCheck
])
@@ -43,7 +43,7 @@ RSpec.describe SystemCheck::IncomingEmailCheck do
it 'runs mailroom checks' do
expect(SystemCheck).to receive(:run).with('Reply by email', [
- SystemCheck::IncomingEmail::InitdConfiguredCheck,
+ SystemCheck::IncomingEmail::MailRoomEnabledCheck,
SystemCheck::IncomingEmail::MailRoomRunningCheck
])
diff --git a/spec/lib/uploaded_file_spec.rb b/spec/lib/uploaded_file_spec.rb
index ececc84bc93..0aba6cb0065 100644
--- a/spec/lib/uploaded_file_spec.rb
+++ b/spec/lib/uploaded_file_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe UploadedFile do
end
context 'from_params functions' do
- RSpec.shared_examples 'using the file path' do |filename:, content_type:, sha256:, path_suffix:|
+ RSpec.shared_examples 'using the file path' do |filename:, content_type:, sha256:, path_suffix:, upload_duration:|
it { is_expected.not_to be_nil }
it 'sets properly the attributes' do
@@ -24,6 +24,7 @@ RSpec.describe UploadedFile do
expect(subject.sha256).to eq(sha256)
expect(subject.remote_id).to be_nil
expect(subject.path).to end_with(path_suffix)
+ expect(subject.upload_duration).to eq(upload_duration)
end
it 'handles a blank path' do
@@ -37,16 +38,17 @@ RSpec.describe UploadedFile do
end
end
- RSpec.shared_examples 'using the remote id' do |filename:, content_type:, sha256:, size:, remote_id:|
+ RSpec.shared_examples 'using the remote id' do |filename:, content_type:, sha256:, size:, remote_id:, upload_duration:|
it { is_expected.not_to be_nil }
it 'sets properly the attributes' do
expect(subject.original_filename).to eq(filename)
- expect(subject.content_type).to eq('application/octet-stream')
- expect(subject.sha256).to eq('sha256')
+ expect(subject.content_type).to eq(content_type)
+ expect(subject.sha256).to eq(sha256)
expect(subject.path).to be_nil
- expect(subject.size).to eq(123456)
- expect(subject.remote_id).to eq('1234567890')
+ expect(subject.size).to eq(size)
+ expect(subject.remote_id).to eq(remote_id)
+ expect(subject.upload_duration).to eq(upload_duration)
end
end
@@ -78,6 +80,7 @@ RSpec.describe UploadedFile do
{ 'path' => temp_file.path,
'name' => 'dir/my file&.txt',
'type' => 'my/type',
+ 'upload_duration' => '5.05',
'sha256' => 'sha256' }
end
@@ -85,7 +88,8 @@ RSpec.describe UploadedFile do
filename: 'my_file_.txt',
content_type: 'my/type',
sha256: 'sha256',
- path_suffix: 'test'
+ path_suffix: 'test',
+ upload_duration: 5.05
end
context 'with a remote id' do
@@ -96,6 +100,7 @@ RSpec.describe UploadedFile do
'remote_url' => 'http://localhost/file',
'remote_id' => '1234567890',
'etag' => 'etag1234567890',
+ 'upload_duration' => '5.05',
'size' => '123456'
}
end
@@ -105,7 +110,8 @@ RSpec.describe UploadedFile do
content_type: 'application/octet-stream',
sha256: 'sha256',
size: 123456,
- remote_id: '1234567890'
+ remote_id: '1234567890',
+ upload_duration: 5.05
end
context 'with a path and a remote id' do
@@ -117,6 +123,7 @@ RSpec.describe UploadedFile do
'remote_url' => 'http://localhost/file',
'remote_id' => '1234567890',
'etag' => 'etag1234567890',
+ 'upload_duration' => '5.05',
'size' => '123456'
}
end
@@ -126,7 +133,8 @@ RSpec.describe UploadedFile do
content_type: 'application/octet-stream',
sha256: 'sha256',
size: 123456,
- remote_id: '1234567890'
+ remote_id: '1234567890',
+ upload_duration: 5.05
end
end
end
@@ -216,6 +224,44 @@ RSpec.describe UploadedFile do
end.to raise_error(UploadedFile::UnknownSizeError, 'Unable to determine file size')
end
end
+
+ context 'when upload_duration is not provided' do
+ it 'sets upload_duration to zero' do
+ file = described_class.new(temp_file.path)
+
+ expect(file.upload_duration).to be_zero
+ end
+ end
+
+ context 'when upload_duration is provided' do
+ let(:file) { described_class.new(temp_file.path, upload_duration: duration) }
+
+ context 'and upload_duration is a number' do
+ let(:duration) { 5.505 }
+
+ it 'sets the upload_duration' do
+ expect(file.upload_duration).to eq(duration)
+ end
+ end
+
+ context 'and upload_duration is a string' do
+ context 'and represents a number' do
+ let(:duration) { '5.505' }
+
+ it 'converts upload_duration to a number' do
+ expect(file.upload_duration).to eq(duration.to_f)
+ end
+ end
+
+ context 'and does not represent a number' do
+ let(:duration) { 'not a number' }
+
+ it 'sets upload_duration to zero' do
+ expect(file.upload_duration).to be_zero
+ end
+ end
+ end
+ end
end
describe '#sanitize_filename' do
diff --git a/spec/mailers/emails/in_product_marketing_spec.rb b/spec/mailers/emails/in_product_marketing_spec.rb
index 99beef92dea..3b92b049e42 100644
--- a/spec/mailers/emails/in_product_marketing_spec.rb
+++ b/spec/mailers/emails/in_product_marketing_spec.rb
@@ -47,22 +47,31 @@ RSpec.describe Emails::InProductMarketing do
end
where(:track, :series) do
- :create | 0
- :create | 1
- :create | 2
- :verify | 0
- :verify | 1
- :verify | 2
- :trial | 0
- :trial | 1
- :trial | 2
- :team | 0
- :team | 1
- :team | 2
- :experience | 0
+ :create | 0
+ :create | 1
+ :create | 2
+ :verify | 0
+ :verify | 1
+ :verify | 2
+ :trial | 0
+ :trial | 1
+ :trial | 2
+ :team | 0
+ :team | 1
+ :team | 2
+ :experience | 0
+ :team_short | 0
+ :trial_short | 0
+ :admin_verify | 0
+ :invite_team | 0
end
with_them do
+ before do
+ stub_experiments(invite_members_for_task: :candidate)
+ group.add_owner(user)
+ end
+
it 'has the correct subject and content' do
message = Gitlab::Email::Message::InProductMarketing.for(track).new(group: group, user: user, series: series)
@@ -76,6 +85,20 @@ RSpec.describe Emails::InProductMarketing do
else
is_expected.to have_body_text(CGI.unescapeHTML(message.cta_link))
end
+
+ if track =~ /(create|verify)/
+ is_expected.to have_body_text(message.invite_text)
+ is_expected.to have_body_text(CGI.unescapeHTML(message.invite_link))
+ else
+ is_expected.not_to have_body_text(message.invite_text)
+ is_expected.not_to have_body_text(CGI.unescapeHTML(message.invite_link))
+ end
+
+ if track == :invite_team
+ is_expected.not_to have_body_text(/This is email \d of \d/)
+ else
+ is_expected.to have_body_text(message.progress)
+ end
end
end
end
diff --git a/spec/mailers/emails/pipelines_spec.rb b/spec/mailers/emails/pipelines_spec.rb
index b9bc53625ac..3a2eb105964 100644
--- a/spec/mailers/emails/pipelines_spec.rb
+++ b/spec/mailers/emails/pipelines_spec.rb
@@ -71,10 +71,19 @@ RSpec.describe Emails::Pipelines do
end
end
+ shared_examples_for 'only accepts a single recipient' do
+ let(:recipient) { ['test@gitlab.com', 'test2@gitlab.com'] }
+
+ it 'raises an ArgumentError' do
+ expect { subject.deliver_now }.to raise_error(ArgumentError)
+ end
+ end
+
describe '#pipeline_success_email' do
- subject { Notify.pipeline_success_email(pipeline, pipeline.user.try(:email)) }
+ subject { Notify.pipeline_success_email(pipeline, recipient) }
let(:pipeline) { create(:ci_pipeline, project: project, ref: ref, sha: sha) }
+ let(:recipient) { pipeline.user.try(:email) }
let(:ref) { 'master' }
let(:sha) { project.commit(ref).sha }
@@ -93,12 +102,15 @@ RSpec.describe Emails::Pipelines do
stub_config_setting(email_subject_suffix: email_subject_suffix)
end
end
+
+ it_behaves_like 'only accepts a single recipient'
end
describe '#pipeline_failed_email' do
- subject { Notify.pipeline_failed_email(pipeline, pipeline.user.try(:email)) }
+ subject { Notify.pipeline_failed_email(pipeline, recipient) }
let(:pipeline) { create(:ci_pipeline, project: project, ref: ref, sha: sha) }
+ let(:recipient) { pipeline.user.try(:email) }
let(:ref) { 'master' }
let(:sha) { project.commit(ref).sha }
@@ -106,12 +118,15 @@ RSpec.describe Emails::Pipelines do
let(:status) { 'Failed' }
let(:status_text) { "Pipeline ##{pipeline.id} has failed!" }
end
+
+ it_behaves_like 'only accepts a single recipient'
end
describe '#pipeline_fixed_email' do
subject { Notify.pipeline_fixed_email(pipeline, pipeline.user.try(:email)) }
let(:pipeline) { create(:ci_pipeline, project: project, ref: ref, sha: sha) }
+ let(:recipient) { pipeline.user.try(:email) }
let(:ref) { 'master' }
let(:sha) { project.commit(ref).sha }
@@ -119,5 +134,7 @@ RSpec.describe Emails::Pipelines do
let(:status) { 'Fixed' }
let(:status_text) { "Pipeline has been fixed and ##{pipeline.id} has passed!" }
end
+
+ it_behaves_like 'only accepts a single recipient'
end
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index f39037cf744..a5e3350ec2e 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -8,6 +8,7 @@ RSpec.describe Notify do
include EmailSpec::Matchers
include EmailHelpers
include RepoHelpers
+ include MembersHelper
include_context 'gitlab email notification'
@@ -720,11 +721,8 @@ RSpec.describe Notify do
end
describe 'project access denied' do
- let(:project) { create(:project, :public) }
- let(:project_member) do
- project.request_access(user)
- project.requesters.find_by(user_id: user.id)
- end
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:project_member) { create(:project_member, :developer, :access_request, user: user, source: project) }
subject { described_class.member_access_denied_email('project', project.id, user.id) }
@@ -739,6 +737,17 @@ RSpec.describe Notify do
is_expected.to have_body_text project.full_name
is_expected.to have_body_text project.web_url
end
+
+ context 'when user can not read project' do
+ let_it_be(:project) { create(:project, :private) }
+
+ it 'hides project name from subject and body' do
+ is_expected.to have_subject "Access to the Hidden project was denied"
+ is_expected.to have_body_text "Hidden project"
+ is_expected.not_to have_body_text project.full_name
+ is_expected.not_to have_body_text project.web_url
+ end
+ end
end
describe 'project access changed' do
@@ -761,10 +770,21 @@ RSpec.describe Notify do
is_expected.to have_body_text project_member.human_access
is_expected.to have_body_text 'leave the project'
is_expected.to have_body_text project_url(project, leave: 1)
+ is_expected.not_to have_body_text 'You were assigned the following tasks:'
+ end
+
+ context 'with tasks to be done present' do
+ let(:project_member) { create(:project_member, project: project, user: user, tasks_to_be_done: [:ci, :code]) }
+
+ it 'contains the assigned tasks to be done' do
+ is_expected.to have_body_text 'You were assigned the following tasks:'
+ is_expected.to have_body_text localized_tasks_to_be_done_choices[:ci]
+ is_expected.to have_body_text localized_tasks_to_be_done_choices[:code]
+ end
end
end
- def invite_to_project(project, inviter:, user: nil)
+ def invite_to_project(project, inviter:, user: nil, tasks_to_be_done: [])
create(
:project_member,
:developer,
@@ -772,7 +792,8 @@ RSpec.describe Notify do
invite_token: '1234',
invite_email: 'toto@example.com',
user: user,
- created_by: inviter
+ created_by: inviter,
+ tasks_to_be_done: tasks_to_be_done
)
end
@@ -804,6 +825,7 @@ RSpec.describe Notify do
is_expected.to have_content("#{inviter.name} invited you to join the")
is_expected.to have_content('Project details')
is_expected.to have_content("What's it about?")
+ is_expected.not_to have_body_text 'and has assigned you the following tasks:'
end
end
@@ -890,6 +912,16 @@ RSpec.describe Notify do
end
end
end
+
+ context 'with tasks to be done present', :aggregate_failures do
+ let(:project_member) { invite_to_project(project, inviter: inviter, tasks_to_be_done: [:ci, :code]) }
+
+ it 'contains the assigned tasks to be done' do
+ is_expected.to have_body_text 'and has assigned you the following tasks:'
+ is_expected.to have_body_text localized_tasks_to_be_done_choices[:ci]
+ is_expected.to have_body_text localized_tasks_to_be_done_choices[:code]
+ end
+ end
end
describe 'project invitation accepted' do
@@ -1351,10 +1383,8 @@ RSpec.describe Notify do
end
describe 'group access denied' do
- let(:group_member) do
- group.request_access(user)
- group.requesters.find_by(user_id: user.id)
- end
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:group_member) { create(:group_member, :developer, :access_request, user: user, source: group) }
let(:recipient) { user }
@@ -1372,6 +1402,17 @@ RSpec.describe Notify do
is_expected.to have_body_text group.name
is_expected.to have_body_text group.web_url
end
+
+ context 'when user can not read group' do
+ let_it_be(:group) { create(:group, :private) }
+
+ it 'hides group name from subject and body' do
+ is_expected.to have_subject "Access to the Hidden group was denied"
+ is_expected.to have_body_text "Hidden group"
+ is_expected.not_to have_body_text group.name
+ is_expected.not_to have_body_text group.web_url
+ end
+ end
end
describe 'group access changed' do
@@ -1398,7 +1439,7 @@ RSpec.describe Notify do
end
end
- def invite_to_group(group, inviter:, user: nil)
+ def invite_to_group(group, inviter:, user: nil, tasks_to_be_done: [])
create(
:group_member,
:developer,
@@ -1406,7 +1447,8 @@ RSpec.describe Notify do
invite_token: '1234',
invite_email: 'toto@example.com',
user: user,
- created_by: inviter
+ created_by: inviter,
+ tasks_to_be_done: tasks_to_be_done
)
end
@@ -1431,6 +1473,7 @@ RSpec.describe Notify do
is_expected.to have_body_text group.name
is_expected.to have_body_text group_member.human_access.downcase
is_expected.to have_body_text group_member.invite_token
+ is_expected.not_to have_body_text 'and has assigned you the following tasks:'
end
end
@@ -1444,6 +1487,24 @@ RSpec.describe Notify do
is_expected.to have_body_text group_member.invite_token
end
end
+
+ context 'with tasks to be done present', :aggregate_failures do
+ let(:group_member) { invite_to_group(group, inviter: inviter, tasks_to_be_done: [:ci, :code]) }
+
+ it 'contains the assigned tasks to be done' do
+ is_expected.to have_body_text 'and has assigned you the following tasks:'
+ is_expected.to have_body_text localized_tasks_to_be_done_choices[:ci]
+ is_expected.to have_body_text localized_tasks_to_be_done_choices[:code]
+ end
+
+ context 'when there is no inviter' do
+ let(:inviter) { nil }
+
+ it 'does not contain the assigned tasks to be done' do
+ is_expected.not_to have_body_text 'and has assigned you the following tasks:'
+ end
+ end
+ end
end
describe 'group invitation reminders' do
diff --git a/spec/migrations/20200107172020_add_timestamp_softwarelicensespolicy_spec.rb b/spec/migrations/20200107172020_add_timestamp_softwarelicensespolicy_spec.rb
deleted file mode 100644
index fff0745e8af..00000000000
--- a/spec/migrations/20200107172020_add_timestamp_softwarelicensespolicy_spec.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!('add_timestamp_softwarelicensespolicy')
-
-RSpec.describe AddTimestampSoftwarelicensespolicy do
- let(:software_licenses_policy) { table(:software_license_policies) }
- let(:projects) { table(:projects) }
- let(:licenses) { table(:software_licenses) }
-
- before do
- projects.create!(name: 'gitlab', path: 'gitlab-org/gitlab-ce', namespace_id: 1)
- licenses.create!(name: 'MIT')
- software_licenses_policy.create!(project_id: projects.first.id, software_license_id: licenses.first.id)
- end
-
- it 'creates timestamps' do
- migrate!
-
- expect(software_licenses_policy.first.created_at).to be_nil
- expect(software_licenses_policy.first.updated_at).to be_nil
- end
-end
diff --git a/spec/migrations/20200122123016_backfill_project_settings_spec.rb b/spec/migrations/20200122123016_backfill_project_settings_spec.rb
deleted file mode 100644
index 7fc8eb0e368..00000000000
--- a/spec/migrations/20200122123016_backfill_project_settings_spec.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!('backfill_project_settings')
-
-RSpec.describe BackfillProjectSettings, :sidekiq, schema: 20200114113341 do
- let(:projects) { table(:projects) }
- let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
- let(:project) { projects.create!(namespace_id: namespace.id) }
-
- describe '#up' do
- before do
- stub_const("#{described_class}::BATCH_SIZE", 2)
-
- projects.create!(id: 1, namespace_id: namespace.id)
- projects.create!(id: 2, namespace_id: namespace.id)
- projects.create!(id: 3, namespace_id: namespace.id)
- end
-
- it 'schedules BackfillProjectSettings background jobs' do
- Sidekiq::Testing.fake! do
- freeze_time do
- migrate!
-
- expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2.minutes, 1, 2)
- expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, 3, 3)
- expect(BackgroundMigrationWorker.jobs.size).to eq(2)
- end
- end
- end
- end
-end
diff --git a/spec/migrations/20200123155929_remove_invalid_jira_data_spec.rb b/spec/migrations/20200123155929_remove_invalid_jira_data_spec.rb
deleted file mode 100644
index 9000d4b7fef..00000000000
--- a/spec/migrations/20200123155929_remove_invalid_jira_data_spec.rb
+++ /dev/null
@@ -1,77 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!('remove_invalid_jira_data')
-
-RSpec.describe RemoveInvalidJiraData do
- let(:jira_tracker_data) { table(:jira_tracker_data) }
- let(:services) { table(:services) }
-
- let(:service) { services.create!(id: 1) }
- let(:data) do
- {
- service_id: service.id,
- encrypted_api_url: 'http:url.com',
- encrypted_api_url_iv: 'somevalue',
- encrypted_url: 'http:url.com',
- encrypted_url_iv: 'somevalue',
- encrypted_username: 'username',
- encrypted_username_iv: 'somevalue',
- encrypted_password: 'username',
- encrypted_password_iv: 'somevalue'
- }
- end
-
- let!(:valid_data) { jira_tracker_data.create!(data) }
- let!(:empty_data) { jira_tracker_data.create!(service_id: service.id) }
- let!(:invalid_api_url) do
- data[:encrypted_api_url_iv] = nil
- jira_tracker_data.create!(data)
- end
-
- let!(:missing_api_url) do
- data[:encrypted_api_url] = ''
- data[:encrypted_api_url_iv] = nil
- jira_tracker_data.create!(data)
- end
-
- let!(:invalid_url) do
- data[:encrypted_url_iv] = nil
- jira_tracker_data.create!(data)
- end
-
- let!(:missing_url) do
- data[:encrypted_url] = ''
- jira_tracker_data.create!(data)
- end
-
- let!(:invalid_username) do
- data[:encrypted_username_iv] = nil
- jira_tracker_data.create!(data)
- end
-
- let!(:missing_username) do
- data[:encrypted_username] = nil
- data[:encrypted_username_iv] = nil
- jira_tracker_data.create!(data)
- end
-
- let!(:invalid_password) do
- data[:encrypted_password_iv] = nil
- jira_tracker_data.create!(data)
- end
-
- let!(:missing_password) do
- data[:encrypted_password] = nil
- data[:encrypted_username_iv] = nil
- jira_tracker_data.create!(data)
- end
-
- it 'removes the invalid data' do
- valid_data_records = [valid_data, empty_data, missing_api_url, missing_url, missing_username, missing_password]
-
- expect { migrate! }.to change { jira_tracker_data.count }.from(10).to(6)
-
- expect(jira_tracker_data.all).to match_array(valid_data_records)
- end
-end
diff --git a/spec/migrations/20200127090233_remove_invalid_issue_tracker_data_spec.rb b/spec/migrations/20200127090233_remove_invalid_issue_tracker_data_spec.rb
deleted file mode 100644
index 1d3476d6d61..00000000000
--- a/spec/migrations/20200127090233_remove_invalid_issue_tracker_data_spec.rb
+++ /dev/null
@@ -1,64 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!('remove_invalid_issue_tracker_data')
-
-RSpec.describe RemoveInvalidIssueTrackerData do
- let(:issue_tracker_data) { table(:issue_tracker_data) }
- let(:services) { table(:services) }
-
- let(:service) { services.create!(id: 1) }
- let(:data) do
- {
- service_id: service.id,
- encrypted_issues_url: 'http:url.com',
- encrypted_issues_url_iv: 'somevalue',
- encrypted_new_issue_url: 'http:url.com',
- encrypted_new_issue_url_iv: 'somevalue',
- encrypted_project_url: 'username',
- encrypted_project_url_iv: 'somevalue'
- }
- end
-
- let!(:valid_data) { issue_tracker_data.create!(data) }
- let!(:empty_data) { issue_tracker_data.create!(service_id: service.id) }
- let!(:invalid_issues_url) do
- data[:encrypted_issues_url_iv] = nil
- issue_tracker_data.create!(data)
- end
-
- let!(:missing_issues_url) do
- data[:encrypted_issues_url] = ''
- data[:encrypted_issues_url_iv] = nil
- issue_tracker_data.create!(data)
- end
-
- let!(:invalid_new_isue_url) do
- data[:encrypted_new_issue_url_iv] = nil
- issue_tracker_data.create!(data)
- end
-
- let!(:missing_new_issue_url) do
- data[:encrypted_new_issue_url] = ''
- issue_tracker_data.create!(data)
- end
-
- let!(:invalid_project_url) do
- data[:encrypted_project_url_iv] = nil
- issue_tracker_data.create!(data)
- end
-
- let!(:missing_project_url) do
- data[:encrypted_project_url] = nil
- data[:encrypted_project_url_iv] = nil
- issue_tracker_data.create!(data)
- end
-
- it 'removes the invalid data' do
- valid_data_records = [valid_data, empty_data, missing_issues_url, missing_new_issue_url, missing_project_url]
-
- expect { migrate! }.to change { issue_tracker_data.count }.from(8).to(5)
-
- expect(issue_tracker_data.all).to match_array(valid_data_records)
- end
-end
diff --git a/spec/migrations/20200130145430_reschedule_migrate_issue_trackers_data_spec.rb b/spec/migrations/20200130145430_reschedule_migrate_issue_trackers_data_spec.rb
deleted file mode 100644
index cf8bc608483..00000000000
--- a/spec/migrations/20200130145430_reschedule_migrate_issue_trackers_data_spec.rb
+++ /dev/null
@@ -1,115 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!('reschedule_migrate_issue_trackers_data')
-
-RSpec.describe RescheduleMigrateIssueTrackersData do
- let(:services) { table(:services) }
- let(:migration_class) { Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData }
- let(:migration_name) { migration_class.to_s.demodulize }
-
- let(:properties) do
- {
- 'url' => 'http://example.com'
- }
- end
-
- let!(:jira_integration) do
- services.create!(id: 10, type: 'JiraService', properties: properties, category: 'issue_tracker')
- end
-
- let!(:jira_integration_nil) do
- services.create!(id: 11, type: 'JiraService', properties: nil, category: 'issue_tracker')
- end
-
- let!(:bugzilla_integration) do
- services.create!(id: 12, type: 'BugzillaService', properties: properties, category: 'issue_tracker')
- end
-
- let!(:youtrack_integration) do
- services.create!(id: 13, type: 'YoutrackService', properties: properties, category: 'issue_tracker')
- end
-
- let!(:youtrack_integration_empty) do
- services.create!(id: 14, type: 'YoutrackService', properties: '', category: 'issue_tracker')
- end
-
- let!(:gitlab_service) do
- services.create!(id: 15, type: 'GitlabIssueTrackerService', properties: properties, category: 'issue_tracker')
- end
-
- let!(:gitlab_service_empty) do
- services.create!(id: 16, type: 'GitlabIssueTrackerService', properties: {}, category: 'issue_tracker')
- end
-
- let!(:other_service) do
- services.create!(id: 17, type: 'OtherService', properties: properties, category: 'other_category')
- end
-
- before do
- stub_const("#{described_class}::BATCH_SIZE", 2)
- end
-
- describe "#up" do
- it 'schedules background migrations at correct time' do
- Sidekiq::Testing.fake! do
- freeze_time do
- migrate!
-
- expect(migration_name).to be_scheduled_delayed_migration(3.minutes, jira_integration.id, bugzilla_integration.id)
- expect(migration_name).to be_scheduled_delayed_migration(6.minutes, youtrack_integration.id, gitlab_service.id)
- expect(BackgroundMigrationWorker.jobs.size).to eq(2)
- end
- end
- end
- end
-
- describe "#down" do
- let(:issue_tracker_data) { table(:issue_tracker_data) }
- let(:jira_tracker_data) { table(:jira_tracker_data) }
-
- let!(:valid_issue_tracker_data) do
- issue_tracker_data.create!(
- service_id: bugzilla_integration.id,
- encrypted_issues_url: 'http://url.com',
- encrypted_issues_url_iv: 'somevalue'
- )
- end
-
- let!(:invalid_issue_tracker_data) do
- issue_tracker_data.create!(
- service_id: bugzilla_integration.id,
- encrypted_issues_url: 'http:url.com',
- encrypted_issues_url_iv: nil
- )
- end
-
- let!(:valid_jira_tracker_data) do
- jira_tracker_data.create!(
- service_id: bugzilla_integration.id,
- encrypted_url: 'http://url.com',
- encrypted_url_iv: 'somevalue'
- )
- end
-
- let!(:invalid_jira_tracker_data) do
- jira_tracker_data.create!(
- service_id: bugzilla_integration.id,
- encrypted_url: 'http://url.com',
- encrypted_url_iv: nil
- )
- end
-
- it 'removes the invalid jira tracker data' do
- expect { described_class.new.down }.to change { jira_tracker_data.count }.from(2).to(1)
-
- expect(jira_tracker_data.all).to eq([valid_jira_tracker_data])
- end
-
- it 'removes the invalid issue tracker data' do
- expect { described_class.new.down }.to change { issue_tracker_data.count }.from(2).to(1)
-
- expect(issue_tracker_data.all).to eq([valid_issue_tracker_data])
- end
- end
-end
diff --git a/spec/migrations/20200313203550_remove_orphaned_chat_names_spec.rb b/spec/migrations/20200313203550_remove_orphaned_chat_names_spec.rb
deleted file mode 100644
index 6b1126ca53e..00000000000
--- a/spec/migrations/20200313203550_remove_orphaned_chat_names_spec.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!('remove_orphaned_chat_names')
-
-RSpec.describe RemoveOrphanedChatNames, schema: 20200313202430 do
- let(:projects) { table(:projects) }
- let(:namespaces) { table(:namespaces) }
- let(:services) { table(:services) }
- let(:chat_names) { table(:chat_names) }
-
- let(:namespace) { namespaces.create!(name: 'foo', path: 'foo') }
- let(:project) { projects.create!(namespace_id: namespace.id) }
- let(:service) { services.create!(project_id: project.id, type: 'chat') }
- let(:chat_name) { chat_names.create!(service_id: service.id, team_id: 'TEAM', user_id: 12345, chat_id: 12345) }
- let(:orphaned_chat_name) { chat_names.create!(team_id: 'TEAM', service_id: 0, user_id: 12345, chat_id: 12345) }
-
- it 'removes the orphaned chat_name' do
- expect(chat_name).to be_present
- expect(orphaned_chat_name).to be_present
-
- migrate!
-
- expect(chat_names.where(id: orphaned_chat_name.id)).to be_empty
- expect(chat_name.reload).to be_present
- end
-end
diff --git a/spec/migrations/20200406102120_backfill_deployment_clusters_from_deployments_spec.rb b/spec/migrations/20200406102120_backfill_deployment_clusters_from_deployments_spec.rb
deleted file mode 100644
index c6a512a1ec9..00000000000
--- a/spec/migrations/20200406102120_backfill_deployment_clusters_from_deployments_spec.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!('backfill_deployment_clusters_from_deployments')
-
-RSpec.describe BackfillDeploymentClustersFromDeployments, :migration, :sidekiq, schema: 20200227140242 do
- describe '#up' do
- it 'schedules BackfillDeploymentClustersFromDeployments background jobs' do
- stub_const("#{described_class}::BATCH_SIZE", 2)
-
- namespace = table(:namespaces).create!(name: 'the-namespace', path: 'the-path')
- project = table(:projects).create!(name: 'the-project', namespace_id: namespace.id)
- environment = table(:environments).create!(name: 'the-environment', project_id: project.id, slug: 'slug')
- cluster = table(:clusters).create!(name: 'the-cluster')
-
- deployment_data = { cluster_id: cluster.id, project_id: project.id, environment_id: environment.id, ref: 'abc', tag: false, sha: 'sha', status: 1 }
-
- # batch 1
- batch_1_begin = create_deployment(**deployment_data)
- batch_1_end = create_deployment(**deployment_data)
-
- # value that should not be included due to default scope
- create_deployment(**deployment_data, cluster_id: nil)
-
- # batch 2
- batch_2_begin = create_deployment(**deployment_data)
- batch_2_end = create_deployment(**deployment_data)
-
- Sidekiq::Testing.fake! do
- freeze_time do
- migrate!
-
- # batch 1
- expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2.minutes, batch_1_begin.id, batch_1_end.id)
-
- # batch 2
- expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, batch_2_begin.id, batch_2_end.id)
-
- expect(BackgroundMigrationWorker.jobs.size).to eq(2)
- end
- end
- end
-
- def create_deployment(**data)
- @iid ||= 0
- @iid += 1
- table(:deployments).create!(iid: @iid, **data)
- end
- end
-end
diff --git a/spec/migrations/20200511145545_change_variable_interpolation_format_in_common_metrics_spec.rb b/spec/migrations/20200511145545_change_variable_interpolation_format_in_common_metrics_spec.rb
deleted file mode 100644
index e712e555b70..00000000000
--- a/spec/migrations/20200511145545_change_variable_interpolation_format_in_common_metrics_spec.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!('change_variable_interpolation_format_in_common_metrics')
-
-RSpec.describe ChangeVariableInterpolationFormatInCommonMetrics, :migration do
- let(:prometheus_metrics) { table(:prometheus_metrics) }
-
- let!(:common_metric) do
- prometheus_metrics.create!(
- identifier: 'system_metrics_kubernetes_container_memory_total',
- query: 'avg(sum(container_memory_usage_bytes{container_name!="POD",' \
- 'pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"})' \
- ' by (job)) without (job) /1024/1024/1024',
- project_id: nil,
- title: 'Memory Usage (Total)',
- y_label: 'Total Memory Used (GB)',
- unit: 'GB',
- legend: 'Total (GB)',
- group: -5,
- common: true
- )
- end
-
- it 'updates query to use {{}}' do
- expected_query = <<~EOS.chomp
- avg(sum(container_memory_usage_bytes{container!="POD",\
- pod=~"^{{ci_environment_slug}}-(.*)",namespace="{{kube_namespace}}"}) \
- by (job)) without (job) /1024/1024/1024 OR \
- avg(sum(container_memory_usage_bytes{container_name!="POD",\
- pod_name=~"^{{ci_environment_slug}}-(.*)",namespace="{{kube_namespace}}"}) \
- by (job)) without (job) /1024/1024/1024
- EOS
-
- migrate!
-
- expect(common_metric.reload.query).to eq(expected_query)
- end
-end
diff --git a/spec/migrations/20200526115436_dedup_mr_metrics_spec.rb b/spec/migrations/20200526115436_dedup_mr_metrics_spec.rb
deleted file mode 100644
index f16026884f5..00000000000
--- a/spec/migrations/20200526115436_dedup_mr_metrics_spec.rb
+++ /dev/null
@@ -1,68 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!('dedup_mr_metrics')
-
-RSpec.describe DedupMrMetrics, :migration, schema: 20200526013844 do
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:merge_requests) { table(:merge_requests) }
- let(:metrics) { table(:merge_request_metrics) }
- let(:merge_request_params) { { source_branch: 'x', target_branch: 'y', target_project_id: project.id } }
-
- let!(:namespace) { namespaces.create!(name: 'foo', path: 'foo') }
- let!(:project) { projects.create!(namespace_id: namespace.id) }
- let!(:merge_request_1) { merge_requests.create!(merge_request_params) }
- let!(:merge_request_2) { merge_requests.create!(merge_request_params) }
- let!(:merge_request_3) { merge_requests.create!(merge_request_params) }
-
- let!(:duplicated_metrics_1) { metrics.create!(merge_request_id: merge_request_1.id, latest_build_started_at: 1.day.ago, first_deployed_to_production_at: 5.days.ago, updated_at: 2.months.ago) }
- let!(:duplicated_metrics_2) { metrics.create!(merge_request_id: merge_request_1.id, latest_build_started_at: Time.now, merged_at: Time.now, updated_at: 1.month.ago) }
-
- let!(:duplicated_metrics_3) { metrics.create!(merge_request_id: merge_request_3.id, diff_size: 30, commits_count: 20, updated_at: 2.months.ago) }
- let!(:duplicated_metrics_4) { metrics.create!(merge_request_id: merge_request_3.id, added_lines: 5, commits_count: nil, updated_at: 1.month.ago) }
-
- let!(:non_duplicated_metrics) { metrics.create!(merge_request_id: merge_request_2.id, latest_build_started_at: 2.days.ago) }
-
- it 'deduplicates merge_request_metrics table' do
- expect { migrate! }.to change { metrics.count }.from(5).to(3)
- end
-
- it 'merges `duplicated_metrics_1` with `duplicated_metrics_2`' do
- migrate!
-
- expect(metrics.where(id: duplicated_metrics_1.id)).not_to exist
-
- merged_metrics = metrics.find_by(id: duplicated_metrics_2.id)
-
- expect(merged_metrics).to be_present
- expect(merged_metrics.latest_build_started_at).to be_like_time(duplicated_metrics_2.latest_build_started_at)
- expect(merged_metrics.merged_at).to be_like_time(duplicated_metrics_2.merged_at)
- expect(merged_metrics.first_deployed_to_production_at).to be_like_time(duplicated_metrics_1.first_deployed_to_production_at)
- end
-
- it 'merges `duplicated_metrics_3` with `duplicated_metrics_4`' do
- migrate!
-
- expect(metrics.where(id: duplicated_metrics_3.id)).not_to exist
-
- merged_metrics = metrics.find_by(id: duplicated_metrics_4.id)
-
- expect(merged_metrics).to be_present
- expect(merged_metrics.diff_size).to eq(duplicated_metrics_3.diff_size)
- expect(merged_metrics.commits_count).to eq(duplicated_metrics_3.commits_count)
- expect(merged_metrics.added_lines).to eq(duplicated_metrics_4.added_lines)
- end
-
- it 'does not change non duplicated records' do
- expect { migrate! }.not_to change { non_duplicated_metrics.reload.attributes }
- end
-
- it 'does nothing when there are no metrics' do
- metrics.delete_all
-
- migrate!
-
- expect(metrics.count).to eq(0)
- end
-end
diff --git a/spec/migrations/20200526231421_update_index_approval_rule_name_for_code_owners_rule_type_spec.rb b/spec/migrations/20200526231421_update_index_approval_rule_name_for_code_owners_rule_type_spec.rb
deleted file mode 100644
index 9b72559234e..00000000000
--- a/spec/migrations/20200526231421_update_index_approval_rule_name_for_code_owners_rule_type_spec.rb
+++ /dev/null
@@ -1,175 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!('update_index_approval_rule_name_for_code_owners_rule_type')
-
-RSpec.describe UpdateIndexApprovalRuleNameForCodeOwnersRuleType do
- let(:migration) { described_class.new }
-
- let(:approval_rules) { table(:approval_merge_request_rules) }
- let(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab') }
-
- let(:project) do
- table(:projects).create!(
- namespace_id: namespace.id,
- name: 'gitlab',
- path: 'gitlab'
- )
- end
-
- let(:merge_request) do
- table(:merge_requests).create!(
- target_project_id: project.id,
- source_project_id: project.id,
- target_branch: 'feature',
- source_branch: 'master'
- )
- end
-
- let(:index_names) do
- ActiveRecord::Base.connection
- .indexes(:approval_merge_request_rules)
- .collect(&:name)
- end
-
- def create_sectional_approval_rules
- approval_rules.create!(
- merge_request_id: merge_request.id,
- name: "*.rb",
- code_owner: true,
- rule_type: 2,
- section: "First Section"
- )
-
- approval_rules.create!(
- merge_request_id: merge_request.id,
- name: "*.rb",
- code_owner: true,
- rule_type: 2,
- section: "Second Section"
- )
- end
-
- def create_two_matching_nil_section_approval_rules
- 2.times do
- approval_rules.create!(
- merge_request_id: merge_request.id,
- name: "nil_section",
- code_owner: true,
- rule_type: 2
- )
- end
- end
-
- before do
- approval_rules.delete_all
- end
-
- describe "#up" do
- it "creates the new index and removes the 'legacy' indices" do
- # Confirm that existing legacy indices prevent duplicate entries
- #
- expect { create_sectional_approval_rules }
- .to raise_exception(ActiveRecord::RecordNotUnique)
- expect { create_two_matching_nil_section_approval_rules }
- .to raise_exception(ActiveRecord::RecordNotUnique)
-
- approval_rules.delete_all
-
- disable_migrations_output { migrate! }
-
- # After running the migration, expect `section == nil` rules to still be
- # blocked by the legacy indices, but sectional rules are allowed.
- #
- expect { create_sectional_approval_rules }
- .to change { approval_rules.count }.by(2)
- expect { create_two_matching_nil_section_approval_rules }
- .to raise_exception(ActiveRecord::RecordNotUnique)
-
- # Attempt to rerun the creation of sectional rules, and see that sectional
- # rules are unique by section
- #
- expect { create_sectional_approval_rules }
- .to raise_exception(ActiveRecord::RecordNotUnique)
-
- expect(index_names).to include(
- described_class::SECTIONAL_INDEX_NAME,
- described_class::LEGACY_INDEX_NAME_RULE_TYPE,
- described_class::LEGACY_INDEX_NAME_CODE_OWNERS
- )
- end
- end
-
- describe "#down" do
- context "run as FOSS" do
- before do
- expect(Gitlab).to receive(:ee?).twice.and_return(false)
- end
-
- it "recreates legacy indices, but does not invoke EE-specific code" do
- disable_migrations_output { migrate! }
-
- expect(index_names).to include(
- described_class::SECTIONAL_INDEX_NAME,
- described_class::LEGACY_INDEX_NAME_RULE_TYPE,
- described_class::LEGACY_INDEX_NAME_CODE_OWNERS
- )
-
- # Since ApprovalMergeRequestRules are EE-specific, we expect none to be
- # deleted during the migration.
- #
- expect { disable_migrations_output { migration.down } }
- .not_to change { approval_rules.count }
-
- index_names = ActiveRecord::Base.connection
- .indexes(:approval_merge_request_rules)
- .collect(&:name)
-
- expect(index_names).not_to include(described_class::SECTIONAL_INDEX_NAME)
- expect(index_names).to include(
- described_class::LEGACY_INDEX_NAME_RULE_TYPE,
- described_class::LEGACY_INDEX_NAME_CODE_OWNERS
- )
- end
- end
-
- context "EE" do
- it "recreates 'legacy' indices and removes duplicate code owner approval rules" do
- skip("This test is skipped under FOSS") unless Gitlab.ee?
-
- disable_migrations_output { migrate! }
-
- expect { create_sectional_approval_rules }
- .to change { approval_rules.count }.by(2)
- expect { create_two_matching_nil_section_approval_rules }
- .to raise_exception(ActiveRecord::RecordNotUnique)
-
- expect(MergeRequests::SyncCodeOwnerApprovalRules)
- .to receive(:new).with(MergeRequest.find(merge_request.id)).once.and_call_original
-
- # Run the down migration. This will remove the 3 approval rules we create
- # above, and call MergeRequests::SyncCodeOwnerApprovalRules to recreate
- # new ones. However, as there is no CODEOWNERS file in this test
- # context, no approval rules will be created, so we can expect
- # approval_rules.count to be changed by -3.
- #
- expect { disable_migrations_output { migration.down } }
- .to change { approval_rules.count }.by(-3)
-
- # Test that the index does not allow us to create the same rules as the
- # previous sectional index.
- #
- expect { create_sectional_approval_rules }
- .to raise_exception(ActiveRecord::RecordNotUnique)
- expect { create_two_matching_nil_section_approval_rules }
- .to raise_exception(ActiveRecord::RecordNotUnique)
-
- expect(index_names).not_to include(described_class::SECTIONAL_INDEX_NAME)
- expect(index_names).to include(
- described_class::LEGACY_INDEX_NAME_RULE_TYPE,
- described_class::LEGACY_INDEX_NAME_CODE_OWNERS
- )
- end
- end
- end
-end
diff --git a/spec/migrations/20200703125016_backfill_namespace_settings_spec.rb b/spec/migrations/20200703125016_backfill_namespace_settings_spec.rb
deleted file mode 100644
index c9f7a66a0b9..00000000000
--- a/spec/migrations/20200703125016_backfill_namespace_settings_spec.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!('backfill_namespace_settings')
-
-RSpec.describe BackfillNamespaceSettings, :sidekiq, schema: 20200703124823 do
- let(:namespaces) { table(:namespaces) }
-
- describe '#up' do
- before do
- stub_const("#{described_class}::BATCH_SIZE", 2)
-
- namespaces.create!(id: 1, name: 'test1', path: 'test1')
- namespaces.create!(id: 2, name: 'test2', path: 'test2')
- namespaces.create!(id: 3, name: 'test3', path: 'test3')
- end
-
- it 'schedules BackfillNamespaceSettings background jobs' do
- Sidekiq::Testing.fake! do
- freeze_time do
- migrate!
-
- expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2.minutes, 1, 2)
- expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, 3, 3)
- expect(BackgroundMigrationWorker.jobs.size).to eq(2)
- end
- end
- end
- end
-end
diff --git a/spec/migrations/20200706035141_adjust_unique_index_alert_management_alerts_spec.rb b/spec/migrations/20200706035141_adjust_unique_index_alert_management_alerts_spec.rb
deleted file mode 100644
index 121b1729dd2..00000000000
--- a/spec/migrations/20200706035141_adjust_unique_index_alert_management_alerts_spec.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!('adjust_unique_index_alert_management_alerts')
-
-RSpec.describe AdjustUniqueIndexAlertManagementAlerts, :migration do
- let(:migration) { described_class.new }
- let(:alerts) { AlertManagement::Alert }
- let(:project) { create_project }
- let(:other_project) { create_project }
- let(:resolved_state) { 2 }
- let(:triggered_state) { 1 }
- let!(:existing_alert) { create_alert(project, resolved_state, '1234', 1) }
- let!(:p2_alert) { create_alert(other_project, resolved_state, '1234', 1) }
- let!(:p2_alert_diff_fingerprint) { create_alert(other_project, resolved_state, '4567', 2) }
-
- it 'can reverse the migration' do
- expect(existing_alert.fingerprint).not_to eq(nil)
- expect(p2_alert.fingerprint).not_to eq(nil)
- expect(p2_alert_diff_fingerprint.fingerprint).not_to eq(nil)
-
- migrate!
-
- # Adding a second alert with the same fingerprint now that we can
- second_alert = create_alert(project, triggered_state, '1234', 2)
- expect(alerts.count).to eq(4)
-
- schema_migrate_down!
-
- # We keep the alerts, but the oldest ones fingerprint is removed
- expect(alerts.count).to eq(4)
- expect(second_alert.reload.fingerprint).not_to eq(nil)
- expect(p2_alert.fingerprint).not_to eq(nil)
- expect(p2_alert_diff_fingerprint.fingerprint).not_to eq(nil)
- expect(existing_alert.reload.fingerprint).to eq(nil)
- end
-
- def namespace
- @namespace ||= table(:namespaces).create!(name: 'foo', path: 'foo')
- end
-
- def create_project
- table(:projects).create!(namespace_id: namespace.id)
- end
-
- def create_alert(project, status, fingerprint, iid)
- params = {
- title: 'test',
- started_at: Time.current,
- iid: iid,
- project_id: project.id,
- status: status,
- fingerprint: fingerprint
- }
- table(:alert_management_alerts).create!(params)
- end
-end
diff --git a/spec/migrations/20200728080250_replace_unique_index_on_cycle_analytics_stages_spec.rb b/spec/migrations/20200728080250_replace_unique_index_on_cycle_analytics_stages_spec.rb
deleted file mode 100644
index a632065946d..00000000000
--- a/spec/migrations/20200728080250_replace_unique_index_on_cycle_analytics_stages_spec.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!('replace_unique_index_on_cycle_analytics_stages')
-
-RSpec.describe ReplaceUniqueIndexOnCycleAnalyticsStages, :migration, schema: 20200727142337 do
- let(:namespaces) { table(:namespaces) }
- let(:group_value_streams) { table(:analytics_cycle_analytics_group_value_streams) }
- let(:group_stages) { table(:analytics_cycle_analytics_group_stages) }
-
- let(:group) { namespaces.create!(type: 'Group', name: 'test', path: 'test') }
-
- let(:value_stream_1) { group_value_streams.create!(group_id: group.id, name: 'vs1') }
- let(:value_stream_2) { group_value_streams.create!(group_id: group.id, name: 'vs2') }
-
- let(:duplicated_stage_1) { group_stages.create!(group_id: group.id, group_value_stream_id: value_stream_1.id, name: 'stage', start_event_identifier: 1, end_event_identifier: 1) }
- let(:duplicated_stage_2) { group_stages.create!(group_id: group.id, group_value_stream_id: value_stream_2.id, name: 'stage', start_event_identifier: 1, end_event_identifier: 1) }
-
- let(:stage_record) { group_stages.create!(group_id: group.id, group_value_stream_id: value_stream_2.id, name: 'other stage', start_event_identifier: 1, end_event_identifier: 1) }
-
- describe '#down' do
- subject { described_class.new.down }
-
- before do
- described_class.new.up
-
- duplicated_stage_1
- duplicated_stage_2
- stage_record
- end
-
- it 'removes duplicated stage records' do
- subject
-
- stage = group_stages.find_by_id(duplicated_stage_2.id)
- expect(stage).to be_nil
- end
-
- it 'does not change the first duplicated stage record' do
- expect { subject }.not_to change { duplicated_stage_1.reload.attributes }
- end
-
- it 'does not change not duplicated stage record' do
- expect { subject }.not_to change { stage_record.reload.attributes }
- end
- end
-end
diff --git a/spec/migrations/20200728182311_add_o_auth_paths_to_protected_paths_spec.rb b/spec/migrations/20200728182311_add_o_auth_paths_to_protected_paths_spec.rb
deleted file mode 100644
index 5c65d45c6e0..00000000000
--- a/spec/migrations/20200728182311_add_o_auth_paths_to_protected_paths_spec.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!('add_o_auth_paths_to_protected_paths')
-
-RSpec.describe AddOAuthPathsToProtectedPaths do
- subject(:migration) { described_class.new }
-
- let(:application_settings) { table(:application_settings) }
- let(:new_paths) do
- [
- '/oauth/authorize',
- '/oauth/token'
- ]
- end
-
- it 'appends new OAuth paths' do
- application_settings.create!
-
- protected_paths_before = application_settings.first.protected_paths
- protected_paths_after = protected_paths_before + new_paths
-
- expect { migrate! }.to change { application_settings.first.protected_paths }.from(protected_paths_before).to(protected_paths_after)
- end
-
- it 'new default includes new paths' do
- settings_before = application_settings.create!
-
- expect(settings_before.protected_paths).not_to include(*new_paths)
-
- migrate!
-
- application_settings.reset_column_information
- settings_after = application_settings.create!
-
- expect(settings_after.protected_paths).to include(*new_paths)
- end
-
- it 'does not change the value when the new paths are already included' do
- application_settings.create!(protected_paths: %w(/users/sign_in /users/password) + new_paths)
-
- expect { migrate! }.not_to change { application_settings.first.protected_paths }
- end
-
- it 'adds one value when the other is already present' do
- application_settings.create!(protected_paths: %W(/users/sign_in /users/password #{new_paths.first}))
-
- migrate!
-
- expect(application_settings.first.protected_paths).to include(new_paths.second)
- end
-end
diff --git a/spec/migrations/20200811130433_create_missing_vulnerabilities_issue_links_spec.rb b/spec/migrations/20200811130433_create_missing_vulnerabilities_issue_links_spec.rb
deleted file mode 100644
index d166ff3617b..00000000000
--- a/spec/migrations/20200811130433_create_missing_vulnerabilities_issue_links_spec.rb
+++ /dev/null
@@ -1,160 +0,0 @@
-# frozen_string_literal: true
-require 'spec_helper'
-require_migration!('create_missing_vulnerabilities_issue_links')
-
-RSpec.describe CreateMissingVulnerabilitiesIssueLinks, :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(:different_scanner) { scanners.create!(project_id: project.id, external_id: 'test 2', name: 'test scanner 2') }
- let(:issues) { table(:issues) }
- let(:issue1) { issues.create!(id: 123, project_id: project.id) }
- let(:issue2) { issues.create!(id: 124, project_id: project.id) }
- let(:issue3) { issues.create!(id: 125, project_id: project.id) }
- let(:vulnerabilities) { table(:vulnerabilities) }
- let(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
- let(:vulnerability_feedback) { table(:vulnerability_feedback) }
- let(:vulnerability_issue_links) { table(:vulnerability_issue_links) }
- let(:vulnerability_identifiers) { table(:vulnerability_identifiers) }
- let(:vulnerability_identifier) { vulnerability_identifiers.create!(project_id: project.id, external_type: 'test 1', external_id: 'test 1', fingerprint: 'test 1', name: 'test 1') }
- let(:different_vulnerability_identifier) { vulnerability_identifiers.create!(project_id: project.id, external_type: 'test 2', external_id: 'test 2', fingerprint: 'test 2', name: 'test 2') }
-
- let!(:vulnerability) do
- create_vulnerability!(
- project_id: project.id,
- author_id: user.id
- )
- end
-
- before do
- create_finding!(
- vulnerability_id: vulnerability.id,
- project_id: project.id,
- scanner_id: scanner.id,
- primary_identifier_id: vulnerability_identifier.id
- )
- create_feedback!(
- issue_id: issue1.id,
- project_id: project.id,
- author_id: user.id
- )
-
- # Create a finding with no vulnerability_id
- # https://gitlab.com/gitlab-com/gl-infra/production/-/issues/2539
- create_finding!(
- vulnerability_id: nil,
- project_id: project.id,
- scanner_id: different_scanner.id,
- primary_identifier_id: different_vulnerability_identifier.id,
- location_fingerprint: 'somewhereinspace',
- uuid: 'test2'
- )
- create_feedback!(
- category: 2,
- issue_id: issue2.id,
- project_id: project.id,
- author_id: user.id
- )
- end
-
- context 'with no Vulnerabilities::IssueLinks present' do
- it 'creates missing Vulnerabilities::IssueLinks' do
- expect(vulnerability_issue_links.count).to eq(0)
-
- migrate!
-
- expect(vulnerability_issue_links.count).to eq(1)
- end
- end
-
- context 'when an Vulnerabilities::IssueLink already exists' do
- before do
- vulnerability_issue_links.create!(vulnerability_id: vulnerability.id, issue_id: issue1.id)
- end
-
- it 'creates no duplicates' do
- expect(vulnerability_issue_links.count).to eq(1)
-
- migrate!
-
- expect(vulnerability_issue_links.count).to eq(1)
- end
- end
-
- context 'when an Vulnerabilities::IssueLink of type created already exists' do
- before do
- vulnerability_issue_links.create!(vulnerability_id: vulnerability.id, issue_id: issue3.id, link_type: 2)
- end
-
- it 'creates no duplicates' do
- expect(vulnerability_issue_links.count).to eq(1)
-
- migrate!
-
- expect(vulnerability_issue_links.count).to eq(1)
- end
- 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')
- vulnerabilities_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
-
- # project_fingerprint on Vulnerabilities::Finding is a bytea and we need to match this
- def create_feedback!(issue_id:, project_id:, author_id:, feedback_type: 1, category: 0, project_fingerprint: '3132337177656173647a7863')
- vulnerability_feedback.create!(
- feedback_type: feedback_type,
- issue_id: issue_id,
- category: category,
- project_fingerprint: project_fingerprint,
- project_id: project_id,
- author_id: author_id
- )
- end
-
- 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/20200915044225_schedule_migration_to_hashed_storage_spec.rb b/spec/migrations/20200915044225_schedule_migration_to_hashed_storage_spec.rb
deleted file mode 100644
index 69f7525d265..00000000000
--- a/spec/migrations/20200915044225_schedule_migration_to_hashed_storage_spec.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!('schedule_migration_to_hashed_storage')
-
-RSpec.describe ScheduleMigrationToHashedStorage, :sidekiq do
- describe '#up' do
- it 'schedules background migration job' do
- Sidekiq::Testing.fake! do
- expect { migrate! }.to change { BackgroundMigrationWorker.jobs.size }.by(1)
- end
- end
- end
-end
diff --git a/spec/migrations/20200929052138_create_initial_versions_for_pre_versioning_terraform_states_spec.rb b/spec/migrations/20200929052138_create_initial_versions_for_pre_versioning_terraform_states_spec.rb
deleted file mode 100644
index 34bd8f1c869..00000000000
--- a/spec/migrations/20200929052138_create_initial_versions_for_pre_versioning_terraform_states_spec.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!('create_initial_versions_for_pre_versioning_terraform_states')
-
-RSpec.describe CreateInitialVersionsForPreVersioningTerraformStates do
- let(:namespace) { table(:namespaces).create!(name: 'terraform', path: 'terraform') }
- let(:project) { table(:projects).create!(id: 1, namespace_id: namespace.id) }
- let(:terraform_state_versions) { table(:terraform_state_versions) }
-
- def create_state!(project, versioning_enabled:)
- table(:terraform_states).create!(
- project_id: project.id,
- uuid: 'uuid',
- file_store: 2,
- file: 'state.tfstate',
- versioning_enabled: versioning_enabled
- )
- end
-
- describe '#up' do
- context 'for a state that is already versioned' do
- let!(:terraform_state) { create_state!(project, versioning_enabled: true) }
-
- it 'does not insert a version record' do
- expect { migrate! }.not_to change { terraform_state_versions.count }
- end
- end
-
- context 'for a state that is not yet versioned' do
- let!(:terraform_state) { create_state!(project, versioning_enabled: false) }
-
- it 'creates a version using the current state data' do
- expect { migrate! }.to change { terraform_state_versions.count }.by(1)
-
- migrated_version = terraform_state_versions.last
- expect(migrated_version.terraform_state_id).to eq(terraform_state.id)
- expect(migrated_version.version).to be_zero
- expect(migrated_version.file_store).to eq(terraform_state.file_store)
- expect(migrated_version.file).to eq(terraform_state.file)
- expect(migrated_version.created_at).to be_present
- expect(migrated_version.updated_at).to be_present
- end
- end
- end
-end
diff --git a/spec/migrations/20201014205300_drop_backfill_jira_tracker_deployment_type_jobs_spec.rb b/spec/migrations/20201014205300_drop_backfill_jira_tracker_deployment_type_jobs_spec.rb
deleted file mode 100644
index ef9bc5788c1..00000000000
--- a/spec/migrations/20201014205300_drop_backfill_jira_tracker_deployment_type_jobs_spec.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!('drop_backfill_jira_tracker_deployment_type_jobs')
-
-RSpec.describe DropBackfillJiraTrackerDeploymentTypeJobs, :sidekiq, :redis, schema: 2020_10_14_205300 do
- subject(:migration) { described_class.new }
-
- describe '#up' do
- let(:retry_set) { Sidekiq::RetrySet.new }
- let(:scheduled_set) { Sidekiq::ScheduledSet.new }
-
- context 'there are only affected jobs on the queue' do
- let(:payload) { { 'class' => ::BackgroundMigrationWorker, 'args' => [described_class::DROPPED_JOB_CLASS, 1] } }
- let(:queue_payload) { payload.merge('queue' => described_class::QUEUE) }
-
- it 'removes enqueued BackfillJiraTrackerDeploymentType background jobs' do
- Sidekiq::Testing.disable! do # https://github.com/mperham/sidekiq/wiki/testing#api Sidekiq's API does not have a testing mode
- retry_set.schedule(1.hour.from_now, payload)
- scheduled_set.schedule(1.hour.from_now, payload)
- Sidekiq::Client.push(queue_payload)
-
- expect { migration.up }.to change { Sidekiq::Queue.new(described_class::QUEUE).size }.from(1).to(0)
- expect(retry_set.size).to eq(0)
- expect(scheduled_set.size).to eq(0)
- end
- end
- end
-
- context 'there are not any affected jobs on the queue' do
- let(:payload) { { 'class' => ::BackgroundMigrationWorker, 'args' => ['SomeOtherClass', 1] } }
- let(:queue_payload) { payload.merge('queue' => described_class::QUEUE) }
-
- it 'skips other enqueued jobs' do
- Sidekiq::Testing.disable! do
- retry_set.schedule(1.hour.from_now, payload)
- scheduled_set.schedule(1.hour.from_now, payload)
- Sidekiq::Client.push(queue_payload)
-
- expect { migration.up }.not_to change { Sidekiq::Queue.new(described_class::QUEUE).size }
- expect(retry_set.size).to eq(1)
- expect(scheduled_set.size).to eq(1)
- end
- end
- end
-
- context 'other queues' do
- it 'does not modify them' do
- Sidekiq::Testing.disable! do
- Sidekiq::Client.push('queue' => 'other', 'class' => ::BackgroundMigrationWorker, 'args' => ['SomeOtherClass', 1])
- Sidekiq::Client.push('queue' => 'other', 'class' => ::BackgroundMigrationWorker, 'args' => [described_class::DROPPED_JOB_CLASS, 1])
-
- expect { migration.up }.not_to change { Sidekiq::Queue.new('other').size }
- end
- end
- end
- end
-end
diff --git a/spec/migrations/20201027002551_migrate_services_to_http_integrations_spec.rb b/spec/migrations/20201027002551_migrate_services_to_http_integrations_spec.rb
deleted file mode 100644
index f9f6cd9589c..00000000000
--- a/spec/migrations/20201027002551_migrate_services_to_http_integrations_spec.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!('migrate_services_to_http_integrations')
-
-RSpec.describe MigrateServicesToHttpIntegrations do
- let!(:namespace) { table(:namespaces).create!(name: 'namespace', path: 'namespace') }
- let!(:project) { table(:projects).create!(id: 1, namespace_id: namespace.id) }
- let!(:alert_service) { table(:services).create!(type: 'AlertsService', project_id: project.id, active: true) }
- let!(:alert_service_data) { table(:alerts_service_data).create!(service_id: alert_service.id, encrypted_token: 'test', encrypted_token_iv: 'test')}
- let(:http_integrations) { table(:alert_management_http_integrations) }
-
- describe '#up' do
- it 'creates the http integrations from the alert services', :aggregate_failures do
- expect { migrate! }.to change { http_integrations.count }.by(1)
-
- http_integration = http_integrations.last
- expect(http_integration.project_id).to eq(alert_service.project_id)
- expect(http_integration.encrypted_token).to eq(alert_service_data.encrypted_token)
- expect(http_integration.encrypted_token_iv).to eq(alert_service_data.encrypted_token_iv)
- expect(http_integration.active).to eq(alert_service.active)
- expect(http_integration.name).to eq(described_class::SERVICE_NAMES_IDENTIFIER[:name])
- expect(http_integration.endpoint_identifier).to eq(described_class::SERVICE_NAMES_IDENTIFIER[:identifier])
- end
- end
-end
diff --git a/spec/migrations/20201028182809_backfill_jira_tracker_deployment_type2_spec.rb b/spec/migrations/20201028182809_backfill_jira_tracker_deployment_type2_spec.rb
deleted file mode 100644
index 0746ad7e44f..00000000000
--- a/spec/migrations/20201028182809_backfill_jira_tracker_deployment_type2_spec.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!('backfill_jira_tracker_deployment_type2')
-
-RSpec.describe BackfillJiraTrackerDeploymentType2, :sidekiq, schema: 20201028182809 do
- let(:services) { table(:services) }
- let(:jira_tracker_data) { table(:jira_tracker_data) }
- let(:migration) { described_class.new }
- let(:batch_interval) { described_class::DELAY_INTERVAL }
-
- describe '#up' do
- before do
- stub_const("#{described_class}::BATCH_SIZE", 2)
-
- active_service = services.create!(type: 'JiraService', active: true)
- inactive_service = services.create!(type: 'JiraService', active: false)
-
- jira_tracker_data.create!(id: 1, service_id: active_service.id, deployment_type: 0)
- jira_tracker_data.create!(id: 2, service_id: active_service.id, deployment_type: 1)
- jira_tracker_data.create!(id: 3, service_id: inactive_service.id, deployment_type: 2)
- jira_tracker_data.create!(id: 4, service_id: inactive_service.id, deployment_type: 0)
- jira_tracker_data.create!(id: 5, service_id: active_service.id, deployment_type: 0)
- end
-
- it 'schedules BackfillJiraTrackerDeploymentType2 background jobs' do
- Sidekiq::Testing.fake! do
- freeze_time do
- migration.up
-
- expect(BackgroundMigrationWorker.jobs.size).to eq(2)
- expect(described_class::MIGRATION).to be_scheduled_delayed_migration(batch_interval, 1, 4)
- expect(described_class::MIGRATION).to be_scheduled_delayed_migration(batch_interval * 2, 5, 5)
- end
- end
- end
- end
-end
diff --git a/spec/migrations/20201110161542_cleanup_transfered_projects_shared_runners_spec.rb b/spec/migrations/20201110161542_cleanup_transfered_projects_shared_runners_spec.rb
deleted file mode 100644
index 7a79406ac80..00000000000
--- a/spec/migrations/20201110161542_cleanup_transfered_projects_shared_runners_spec.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!('cleanup_transfered_projects_shared_runners')
-
-RSpec.describe CleanupTransferedProjectsSharedRunners, :sidekiq, schema: 20201110161542 do
- let(:namespaces) { table(:namespaces) }
- let(:migration) { described_class.new }
- let(:batch_interval) { described_class::INTERVAL }
-
- let!(:namespace_1) { namespaces.create!(name: 'foo', path: 'foo') }
- let!(:namespace_2) { namespaces.create!(name: 'bar', path: 'bar') }
- let!(:namespace_3) { namespaces.create!(name: 'baz', path: 'baz') }
-
- describe '#up' do
- before do
- stub_const("#{described_class}::BATCH_SIZE", 2)
- end
-
- it 'schedules ResetSharedRunnersForTransferredProjects background jobs' do
- Sidekiq::Testing.fake! do
- freeze_time do
- migration.up
-
- expect(BackgroundMigrationWorker.jobs.size).to eq(2)
- expect(described_class::MIGRATION).to be_scheduled_delayed_migration(batch_interval, namespace_1.id, namespace_2.id)
- expect(described_class::MIGRATION).to be_scheduled_delayed_migration(batch_interval * 2, namespace_3.id, namespace_3.id)
- end
- end
- end
- end
-end
diff --git a/spec/migrations/20201112130715_schedule_recalculate_uuid_on_vulnerabilities_occurrences_spec.rb b/spec/migrations/20201112130715_schedule_recalculate_uuid_on_vulnerabilities_occurrences_spec.rb
deleted file mode 100644
index dda919d70d9..00000000000
--- a/spec/migrations/20201112130715_schedule_recalculate_uuid_on_vulnerabilities_occurrences_spec.rb
+++ /dev/null
@@ -1,138 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!('schedule_recalculate_uuid_on_vulnerabilities_occurrences')
-
-RSpec.describe ScheduleRecalculateUuidOnVulnerabilitiesOccurrences, :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(:different_scanner) { scanners.create!(project_id: project.id, external_id: 'test 2', name: 'test scanner 2') }
- let(:vulnerabilities) { table(:vulnerabilities) }
- let(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
- let(:vulnerability_identifiers) { table(:vulnerability_identifiers) }
- let(:vulnerability_identifier) do
- vulnerability_identifiers.create!(
- project_id: project.id,
- external_type: 'uuid-v5',
- external_id: 'uuid-v5',
- fingerprint: '7e394d1b1eb461a7406d7b1e08f057a1cf11287a',
- name: 'Identifier for UUIDv5')
- end
-
- let(:different_vulnerability_identifier) do
- vulnerability_identifiers.create!(
- project_id: project.id,
- external_type: 'uuid-v4',
- external_id: 'uuid-v4',
- fingerprint: '772da93d34a1ba010bcb5efa9fb6f8e01bafcc89',
- name: 'Identifier for UUIDv4')
- end
-
- let!(:vulnerability_for_uuidv4) do
- create_vulnerability!(
- project_id: project.id,
- author_id: user.id
- )
- end
-
- let!(:vulnerability_for_uuidv5) do
- create_vulnerability!(
- project_id: project.id,
- author_id: user.id
- )
- end
-
- let(:known_uuid_v4) { "b3cc2518-5446-4dea-871c-89d5e999c1ac" }
- let!(:finding_with_uuid_v4) do
- create_finding!(
- vulnerability_id: vulnerability_for_uuidv4.id,
- project_id: project.id,
- scanner_id: different_scanner.id,
- primary_identifier_id: different_vulnerability_identifier.id,
- report_type: 0, # "sast"
- location_fingerprint: "fa18f432f1d56675f4098d318739c3cd5b14eb3e",
- uuid: known_uuid_v4
- )
- end
-
- let(:known_uuid_v5) { "e7d3d99d-04bb-5771-bb44-d80a9702d0a2" }
- let!(:finding_with_uuid_v5) do
- create_finding!(
- vulnerability_id: vulnerability_for_uuidv5.id,
- project_id: project.id,
- scanner_id: scanner.id,
- primary_identifier_id: vulnerability_identifier.id,
- report_type: 0, # "sast"
- location_fingerprint: "838574be0210968bf6b9f569df9c2576242cbf0a",
- uuid: known_uuid_v5
- )
- 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(2)
- expect(described_class::MIGRATION).to be_scheduled_migration(finding_with_uuid_v4.id, finding_with_uuid_v4.id)
- expect(described_class::MIGRATION).to be_scheduled_migration(finding_with_uuid_v5.id, finding_with_uuid_v5.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')
- vulnerabilities_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/20210112143418_remove_duplicate_services2_spec.rb b/spec/migrations/20210112143418_remove_duplicate_services2_spec.rb
index 289416c22cf..b8dc4d7c8ae 100644
--- a/spec/migrations/20210112143418_remove_duplicate_services2_spec.rb
+++ b/spec/migrations/20210112143418_remove_duplicate_services2_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration!('remove_duplicate_services2')
+require_migration!
RSpec.describe RemoveDuplicateServices2 do
let_it_be(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/20210119122354_alter_vsa_issue_first_mentioned_in_commit_value_spec.rb b/spec/migrations/20210119122354_alter_vsa_issue_first_mentioned_in_commit_value_spec.rb
index 469dbb4f946..e07b5a48909 100644
--- a/spec/migrations/20210119122354_alter_vsa_issue_first_mentioned_in_commit_value_spec.rb
+++ b/spec/migrations/20210119122354_alter_vsa_issue_first_mentioned_in_commit_value_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration!('alter_vsa_issue_first_mentioned_in_commit_value')
+require_migration!
RSpec.describe AlterVsaIssueFirstMentionedInCommitValue, schema: 20210114033715 do
let(:group_stages) { table(:analytics_cycle_analytics_group_stages) }
diff --git a/spec/migrations/20210205174154_remove_bad_dependency_proxy_manifests_spec.rb b/spec/migrations/20210205174154_remove_bad_dependency_proxy_manifests_spec.rb
index cb48df20d58..97438062458 100644
--- a/spec/migrations/20210205174154_remove_bad_dependency_proxy_manifests_spec.rb
+++ b/spec/migrations/20210205174154_remove_bad_dependency_proxy_manifests_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration!('remove_bad_dependency_proxy_manifests')
+require_migration!
RSpec.describe RemoveBadDependencyProxyManifests, schema: 20210128140157 do
let_it_be(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/20210210093901_backfill_updated_at_after_repository_storage_move_spec.rb b/spec/migrations/20210210093901_backfill_updated_at_after_repository_storage_move_spec.rb
index 1932bc00cee..4a31d36e2bc 100644
--- a/spec/migrations/20210210093901_backfill_updated_at_after_repository_storage_move_spec.rb
+++ b/spec/migrations/20210210093901_backfill_updated_at_after_repository_storage_move_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration!('backfill_updated_at_after_repository_storage_move')
+require_migration!
RSpec.describe BackfillUpdatedAtAfterRepositoryStorageMove, :sidekiq do
let_it_be(:projects) { table(:projects) }
diff --git a/spec/migrations/20210218040814_add_environment_scope_to_group_variables_spec.rb b/spec/migrations/20210218040814_add_environment_scope_to_group_variables_spec.rb
index e525101f3a0..039ce53cac4 100644
--- a/spec/migrations/20210218040814_add_environment_scope_to_group_variables_spec.rb
+++ b/spec/migrations/20210218040814_add_environment_scope_to_group_variables_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration!('add_environment_scope_to_group_variables')
+require_migration!
RSpec.describe AddEnvironmentScopeToGroupVariables do
let(:migration) { described_class.new }
diff --git a/spec/migrations/20210226141517_dedup_issue_metrics_spec.rb b/spec/migrations/20210226141517_dedup_issue_metrics_spec.rb
index 6068df85e2e..1b57bf0431f 100644
--- a/spec/migrations/20210226141517_dedup_issue_metrics_spec.rb
+++ b/spec/migrations/20210226141517_dedup_issue_metrics_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration!('dedup_issue_metrics')
+require_migration!
RSpec.describe DedupIssueMetrics, :migration, schema: 20210205104425 do
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/20210406144743_backfill_total_tuple_count_for_batched_migrations_spec.rb b/spec/migrations/20210406144743_backfill_total_tuple_count_for_batched_migrations_spec.rb
index 94ed2320c50..1f18f7e581a 100644
--- a/spec/migrations/20210406144743_backfill_total_tuple_count_for_batched_migrations_spec.rb
+++ b/spec/migrations/20210406144743_backfill_total_tuple_count_for_batched_migrations_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration!('backfill_total_tuple_count_for_batched_migrations')
+require_migration!
RSpec.describe BackfillTotalTupleCountForBatchedMigrations, :migration, schema: 20210406140057 do
let_it_be(:table_name) { 'projects' }
diff --git a/spec/migrations/20210413132500_reschedule_artifact_expiry_backfill_again_spec.rb b/spec/migrations/20210413132500_reschedule_artifact_expiry_backfill_again_spec.rb
index 78b6a71c609..e1dc7487222 100644
--- a/spec/migrations/20210413132500_reschedule_artifact_expiry_backfill_again_spec.rb
+++ b/spec/migrations/20210413132500_reschedule_artifact_expiry_backfill_again_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-require_migration!('reschedule_artifact_expiry_backfill_again')
+require_migration!
RSpec.describe RescheduleArtifactExpiryBackfillAgain, :migration do
let(:migration_class) { Gitlab::BackgroundMigration::BackfillArtifactExpiryDate }
diff --git a/spec/migrations/20210421163509_schedule_update_jira_tracker_data_deployment_type_based_on_url_spec.rb b/spec/migrations/20210421163509_schedule_update_jira_tracker_data_deployment_type_based_on_url_spec.rb
index ea0a16212dd..9a59c739ecd 100644
--- a/spec/migrations/20210421163509_schedule_update_jira_tracker_data_deployment_type_based_on_url_spec.rb
+++ b/spec/migrations/20210421163509_schedule_update_jira_tracker_data_deployment_type_based_on_url_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration!('schedule_update_jira_tracker_data_deployment_type_based_on_url')
+require_migration!
RSpec.describe ScheduleUpdateJiraTrackerDataDeploymentTypeBasedOnUrl, :migration do
let(:services_table) { table(:services) }
diff --git a/spec/migrations/20210423160427_schedule_drop_invalid_vulnerabilities_spec.rb b/spec/migrations/20210423160427_schedule_drop_invalid_vulnerabilities_spec.rb
index 3b462c884c4..faf440eb117 100644
--- a/spec/migrations/20210423160427_schedule_drop_invalid_vulnerabilities_spec.rb
+++ b/spec/migrations/20210423160427_schedule_drop_invalid_vulnerabilities_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration!('schedule_drop_invalid_vulnerabilities')
+require_migration!
RSpec.describe ScheduleDropInvalidVulnerabilities, :migration do
let_it_be(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
diff --git a/spec/migrations/20210430134202_copy_adoption_snapshot_namespace_spec.rb b/spec/migrations/20210430134202_copy_adoption_snapshot_namespace_spec.rb
index 03ce0a430e5..598da495195 100644
--- a/spec/migrations/20210430134202_copy_adoption_snapshot_namespace_spec.rb
+++ b/spec/migrations/20210430134202_copy_adoption_snapshot_namespace_spec.rb
@@ -2,7 +2,7 @@
#
require 'spec_helper'
-require_migration!('copy_adoption_snapshot_namespace')
+require_migration!
RSpec.describe CopyAdoptionSnapshotNamespace, :migration, schema: 20210430124630 do
let(:namespaces_table) { table(:namespaces) }
diff --git a/spec/migrations/20210430135954_copy_adoption_segments_namespace_spec.rb b/spec/migrations/20210430135954_copy_adoption_segments_namespace_spec.rb
index abdfd03f97e..25dfaa2e314 100644
--- a/spec/migrations/20210430135954_copy_adoption_segments_namespace_spec.rb
+++ b/spec/migrations/20210430135954_copy_adoption_segments_namespace_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-require_migration!('copy_adoption_segments_namespace')
+require_migration!
RSpec.describe CopyAdoptionSegmentsNamespace, :migration do
let(:namespaces_table) { table(:namespaces) }
diff --git a/spec/migrations/20210503105845_add_project_value_stream_id_to_project_stages_spec.rb b/spec/migrations/20210503105845_add_project_value_stream_id_to_project_stages_spec.rb
index 4969d82d183..187b9115ba7 100644
--- a/spec/migrations/20210503105845_add_project_value_stream_id_to_project_stages_spec.rb
+++ b/spec/migrations/20210503105845_add_project_value_stream_id_to_project_stages_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-require_migration!('add_project_value_stream_id_to_project_stages')
+require_migration!
RSpec.describe AddProjectValueStreamIdToProjectStages, schema: 20210503105022 do
let(:stages) { table(:analytics_cycle_analytics_project_stages) }
diff --git a/spec/migrations/20210511142748_schedule_drop_invalid_vulnerabilities2_spec.rb b/spec/migrations/20210511142748_schedule_drop_invalid_vulnerabilities2_spec.rb
index 969a2e58947..dd557c833f3 100644
--- a/spec/migrations/20210511142748_schedule_drop_invalid_vulnerabilities2_spec.rb
+++ b/spec/migrations/20210511142748_schedule_drop_invalid_vulnerabilities2_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration!('schedule_drop_invalid_vulnerabilities2')
+require_migration!
RSpec.describe ScheduleDropInvalidVulnerabilities2, :migration do
let_it_be(:background_migration_jobs) { table(:background_migration_jobs) }
diff --git a/spec/migrations/20210514063252_schedule_cleanup_orphaned_lfs_objects_projects_spec.rb b/spec/migrations/20210514063252_schedule_cleanup_orphaned_lfs_objects_projects_spec.rb
index b7524ee0bff..4ac4af19eb9 100644
--- a/spec/migrations/20210514063252_schedule_cleanup_orphaned_lfs_objects_projects_spec.rb
+++ b/spec/migrations/20210514063252_schedule_cleanup_orphaned_lfs_objects_projects_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration!('schedule_cleanup_orphaned_lfs_objects_projects')
+require_migration!
RSpec.describe ScheduleCleanupOrphanedLfsObjectsProjects, schema: 20210511165250 do
let(:lfs_objects_projects) { table(:lfs_objects_projects) }
diff --git a/spec/migrations/20210601073400_fix_total_stage_in_vsa_spec.rb b/spec/migrations/20210601073400_fix_total_stage_in_vsa_spec.rb
index 36d85d1f745..fa4b747aaed 100644
--- a/spec/migrations/20210601073400_fix_total_stage_in_vsa_spec.rb
+++ b/spec/migrations/20210601073400_fix_total_stage_in_vsa_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration!('fix_total_stage_in_vsa')
+require_migration!
RSpec.describe FixTotalStageInVsa, :migration, schema: 20210518001450 do
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/20210601080039_group_protected_environments_add_index_and_constraint_spec.rb b/spec/migrations/20210601080039_group_protected_environments_add_index_and_constraint_spec.rb
index d3154596b26..8d45f571969 100644
--- a/spec/migrations/20210601080039_group_protected_environments_add_index_and_constraint_spec.rb
+++ b/spec/migrations/20210601080039_group_protected_environments_add_index_and_constraint_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration!('group_protected_environments_add_index_and_constraint')
+require_migration!
RSpec.describe GroupProtectedEnvironmentsAddIndexAndConstraint do
let(:migration) { described_class.new }
diff --git a/spec/migrations/20210603222333_remove_builds_email_service_from_services_spec.rb b/spec/migrations/20210603222333_remove_builds_email_service_from_services_spec.rb
index c457be79834..14aa4fe8da7 100644
--- a/spec/migrations/20210603222333_remove_builds_email_service_from_services_spec.rb
+++ b/spec/migrations/20210603222333_remove_builds_email_service_from_services_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-require_migration!('remove_builds_email_service_from_services')
+require_migration!
RSpec.describe RemoveBuildsEmailServiceFromServices do
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/20210610153556_delete_legacy_operations_feature_flags_spec.rb b/spec/migrations/20210610153556_delete_legacy_operations_feature_flags_spec.rb
index 4f621d0670c..17599e75947 100644
--- a/spec/migrations/20210610153556_delete_legacy_operations_feature_flags_spec.rb
+++ b/spec/migrations/20210610153556_delete_legacy_operations_feature_flags_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-require_migration!('delete_legacy_operations_feature_flags')
+require_migration!
RSpec.describe DeleteLegacyOperationsFeatureFlags do
let(:namespace) { table(:namespaces).create!(name: 'foo', path: 'bar') }
diff --git a/spec/migrations/2021061716138_cascade_delete_freeze_periods_spec.rb b/spec/migrations/2021061716138_cascade_delete_freeze_periods_spec.rb
index fd664d99f06..d35184e78a8 100644
--- a/spec/migrations/2021061716138_cascade_delete_freeze_periods_spec.rb
+++ b/spec/migrations/2021061716138_cascade_delete_freeze_periods_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-require_migration!('cascade_delete_freeze_periods')
+require_migration!
RSpec.describe CascadeDeleteFreezePeriods do
let(:namespace) { table(:namespaces).create!(name: 'deploy_freeze', path: 'deploy_freeze') }
diff --git a/spec/migrations/20210708130419_reschedule_merge_request_diff_users_background_migration_spec.rb b/spec/migrations/20210708130419_reschedule_merge_request_diff_users_background_migration_spec.rb
index 9cc454662f9..7a281611650 100644
--- a/spec/migrations/20210708130419_reschedule_merge_request_diff_users_background_migration_spec.rb
+++ b/spec/migrations/20210708130419_reschedule_merge_request_diff_users_background_migration_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration! 'reschedule_merge_request_diff_users_background_migration'
+require_migration!
RSpec.describe RescheduleMergeRequestDiffUsersBackgroundMigration, :migration do
let(:migration) { described_class.new }
diff --git a/spec/migrations/20210722042939_update_issuable_slas_where_issue_closed_spec.rb b/spec/migrations/20210722042939_update_issuable_slas_where_issue_closed_spec.rb
index a0aae00776d..63802acceb5 100644
--- a/spec/migrations/20210722042939_update_issuable_slas_where_issue_closed_spec.rb
+++ b/spec/migrations/20210722042939_update_issuable_slas_where_issue_closed_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration!('update_issuable_slas_where_issue_closed')
+require_migration!
RSpec.describe UpdateIssuableSlasWhereIssueClosed, :migration do
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/20210722150102_operations_feature_flags_correct_flexible_rollout_values_spec.rb b/spec/migrations/20210722150102_operations_feature_flags_correct_flexible_rollout_values_spec.rb
index 130ad45ffc1..94af2bb1e9a 100644
--- a/spec/migrations/20210722150102_operations_feature_flags_correct_flexible_rollout_values_spec.rb
+++ b/spec/migrations/20210722150102_operations_feature_flags_correct_flexible_rollout_values_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-require_migration!('operations_feature_flags_correct_flexible_rollout_values')
+require_migration!
RSpec.describe OperationsFeatureFlagsCorrectFlexibleRolloutValues, :migration do
let_it_be(:strategies) { table(:operations_strategies) }
diff --git a/spec/migrations/20210804150320_create_base_work_item_types_spec.rb b/spec/migrations/20210804150320_create_base_work_item_types_spec.rb
index 9ba29637e00..34ea7f53f51 100644
--- a/spec/migrations/20210804150320_create_base_work_item_types_spec.rb
+++ b/spec/migrations/20210804150320_create_base_work_item_types_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration!('create_base_work_item_types')
+require_migration!
RSpec.describe CreateBaseWorkItemTypes, :migration do
let!(:work_item_types) { table(:work_item_types) }
diff --git a/spec/migrations/20210805192450_update_trial_plans_ci_daily_pipeline_schedule_triggers_spec.rb b/spec/migrations/20210805192450_update_trial_plans_ci_daily_pipeline_schedule_triggers_spec.rb
index 819120d43ef..0b2f76baf1a 100644
--- a/spec/migrations/20210805192450_update_trial_plans_ci_daily_pipeline_schedule_triggers_spec.rb
+++ b/spec/migrations/20210805192450_update_trial_plans_ci_daily_pipeline_schedule_triggers_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-require_migration!('update_trial_plans_ci_daily_pipeline_schedule_triggers')
+require_migration!
RSpec.describe UpdateTrialPlansCiDailyPipelineScheduleTriggers, :migration do
let!(:plans) { table(:plans) }
diff --git a/spec/migrations/20210811122206_update_external_project_bots_spec.rb b/spec/migrations/20210811122206_update_external_project_bots_spec.rb
index a9c7b485cc6..365fb8e3218 100644
--- a/spec/migrations/20210811122206_update_external_project_bots_spec.rb
+++ b/spec/migrations/20210811122206_update_external_project_bots_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration!('update_external_project_bots')
+require_migration!
RSpec.describe UpdateExternalProjectBots, :migration do
def create_user(**extra_options)
diff --git a/spec/migrations/20210818185845_backfill_projects_with_coverage_spec.rb b/spec/migrations/20210818185845_backfill_projects_with_coverage_spec.rb
index d87f952b5da..29f554a003b 100644
--- a/spec/migrations/20210818185845_backfill_projects_with_coverage_spec.rb
+++ b/spec/migrations/20210818185845_backfill_projects_with_coverage_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration!('backfill_projects_with_coverage')
+require_migration!
RSpec.describe BackfillProjectsWithCoverage do
let(:projects) { table(:projects) }
diff --git a/spec/migrations/20210819145000_drop_temporary_columns_and_triggers_for_ci_builds_runner_session_spec.rb b/spec/migrations/20210819145000_drop_temporary_columns_and_triggers_for_ci_builds_runner_session_spec.rb
index b1751216732..4ad4bea058b 100644
--- a/spec/migrations/20210819145000_drop_temporary_columns_and_triggers_for_ci_builds_runner_session_spec.rb
+++ b/spec/migrations/20210819145000_drop_temporary_columns_and_triggers_for_ci_builds_runner_session_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration!('drop_temporary_columns_and_triggers_for_ci_builds_runner_session')
+require_migration!
RSpec.describe DropTemporaryColumnsAndTriggersForCiBuildsRunnerSession, :migration do
let(:ci_builds_runner_session_table) { table(:ci_builds_runner_session) }
diff --git a/spec/migrations/20210831203408_upsert_base_work_item_types_spec.rb b/spec/migrations/20210831203408_upsert_base_work_item_types_spec.rb
index c23110750c3..3c8c55ccb80 100644
--- a/spec/migrations/20210831203408_upsert_base_work_item_types_spec.rb
+++ b/spec/migrations/20210831203408_upsert_base_work_item_types_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration!('upsert_base_work_item_types')
+require_migration!
RSpec.describe UpsertBaseWorkItemTypes, :migration do
let!(:work_item_types) { table(:work_item_types) }
diff --git a/spec/migrations/20210902144144_drop_temporary_columns_and_triggers_for_ci_build_needs_spec.rb b/spec/migrations/20210902144144_drop_temporary_columns_and_triggers_for_ci_build_needs_spec.rb
index 1b35982c41d..4ec3c5b7211 100644
--- a/spec/migrations/20210902144144_drop_temporary_columns_and_triggers_for_ci_build_needs_spec.rb
+++ b/spec/migrations/20210902144144_drop_temporary_columns_and_triggers_for_ci_build_needs_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration!('drop_temporary_columns_and_triggers_for_ci_build_needs')
+require_migration!
RSpec.describe DropTemporaryColumnsAndTriggersForCiBuildNeeds do
let(:ci_build_needs_table) { table(:ci_build_needs) }
diff --git a/spec/migrations/20210906100316_drop_temporary_columns_and_triggers_for_ci_build_trace_chunks_spec.rb b/spec/migrations/20210906100316_drop_temporary_columns_and_triggers_for_ci_build_trace_chunks_spec.rb
index 8d46ba7eb58..f1408e4ecab 100644
--- a/spec/migrations/20210906100316_drop_temporary_columns_and_triggers_for_ci_build_trace_chunks_spec.rb
+++ b/spec/migrations/20210906100316_drop_temporary_columns_and_triggers_for_ci_build_trace_chunks_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration!('drop_temporary_columns_and_triggers_for_ci_build_trace_chunks')
+require_migration!
RSpec.describe DropTemporaryColumnsAndTriggersForCiBuildTraceChunks do
let(:ci_build_trace_chunks_table) { table(:ci_build_trace_chunks) }
diff --git a/spec/migrations/20210906130643_drop_temporary_columns_and_triggers_for_taggings_spec.rb b/spec/migrations/20210906130643_drop_temporary_columns_and_triggers_for_taggings_spec.rb
index 2e7ce733373..e4385e501b2 100644
--- a/spec/migrations/20210906130643_drop_temporary_columns_and_triggers_for_taggings_spec.rb
+++ b/spec/migrations/20210906130643_drop_temporary_columns_and_triggers_for_taggings_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration!('drop_temporary_columns_and_triggers_for_taggings')
+require_migration!
RSpec.describe DropTemporaryColumnsAndTriggersForTaggings do
let(:taggings_table) { table(:taggings) }
diff --git a/spec/migrations/20210907013944_cleanup_bigint_conversion_for_ci_builds_metadata_spec.rb b/spec/migrations/20210907013944_cleanup_bigint_conversion_for_ci_builds_metadata_spec.rb
index ece5ed8251d..194832fbc43 100644
--- a/spec/migrations/20210907013944_cleanup_bigint_conversion_for_ci_builds_metadata_spec.rb
+++ b/spec/migrations/20210907013944_cleanup_bigint_conversion_for_ci_builds_metadata_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration!('cleanup_bigint_conversion_for_ci_builds_metadata')
+require_migration!
RSpec.describe CleanupBigintConversionForCiBuildsMetadata do
let(:ci_builds_metadata) { table(:ci_builds_metadata) }
diff --git a/spec/migrations/20210907211557_finalize_ci_builds_bigint_conversion_spec.rb b/spec/migrations/20210907211557_finalize_ci_builds_bigint_conversion_spec.rb
index 362b4be1bc6..c0f56da7b4f 100644
--- a/spec/migrations/20210907211557_finalize_ci_builds_bigint_conversion_spec.rb
+++ b/spec/migrations/20210907211557_finalize_ci_builds_bigint_conversion_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration!('finalize_ci_builds_bigint_conversion')
+require_migration!
RSpec.describe FinalizeCiBuildsBigintConversion, :migration, schema: 20210907182359 do
context 'with an unexpected FK fk_3f0c88d7dc' do
diff --git a/spec/migrations/20210910194952_update_report_type_for_existing_approval_project_rules_spec.rb b/spec/migrations/20210910194952_update_report_type_for_existing_approval_project_rules_spec.rb
index 46a6d8d92ec..c90eabbe4eb 100644
--- a/spec/migrations/20210910194952_update_report_type_for_existing_approval_project_rules_spec.rb
+++ b/spec/migrations/20210910194952_update_report_type_for_existing_approval_project_rules_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration!('update_report_type_for_existing_approval_project_rules')
+require_migration!
RSpec.describe UpdateReportTypeForExistingApprovalProjectRules, :migration do
using RSpec::Parameterized::TableSyntax
diff --git a/spec/migrations/20210914095310_cleanup_orphan_project_access_tokens_spec.rb b/spec/migrations/20210914095310_cleanup_orphan_project_access_tokens_spec.rb
index 0d0f6a3df67..2b755dfe11c 100644
--- a/spec/migrations/20210914095310_cleanup_orphan_project_access_tokens_spec.rb
+++ b/spec/migrations/20210914095310_cleanup_orphan_project_access_tokens_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration!('cleanup_orphan_project_access_tokens')
+require_migration!
RSpec.describe CleanupOrphanProjectAccessTokens, :migration do
def create_user(**extra_options)
diff --git a/spec/migrations/20210915022415_cleanup_bigint_conversion_for_ci_builds_spec.rb b/spec/migrations/20210915022415_cleanup_bigint_conversion_for_ci_builds_spec.rb
index ee71322433d..cedc62a6565 100644
--- a/spec/migrations/20210915022415_cleanup_bigint_conversion_for_ci_builds_spec.rb
+++ b/spec/migrations/20210915022415_cleanup_bigint_conversion_for_ci_builds_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration!('cleanup_bigint_conversion_for_ci_builds')
+require_migration!
RSpec.describe CleanupBigintConversionForCiBuilds do
let(:ci_builds) { table(:ci_builds) }
diff --git a/spec/migrations/20210922021816_drop_int4_columns_for_ci_job_artifacts_spec.rb b/spec/migrations/20210922021816_drop_int4_columns_for_ci_job_artifacts_spec.rb
index cf326cf0c0a..a6eede8a8f1 100644
--- a/spec/migrations/20210922021816_drop_int4_columns_for_ci_job_artifacts_spec.rb
+++ b/spec/migrations/20210922021816_drop_int4_columns_for_ci_job_artifacts_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration!('drop_int4_columns_for_ci_job_artifacts')
+require_migration!
RSpec.describe DropInt4ColumnsForCiJobArtifacts do
let(:ci_job_artifacts) { table(:ci_job_artifacts) }
diff --git a/spec/migrations/20210922025631_drop_int4_column_for_ci_sources_pipelines_spec.rb b/spec/migrations/20210922025631_drop_int4_column_for_ci_sources_pipelines_spec.rb
index 00b922ee4f8..730c9ade1fb 100644
--- a/spec/migrations/20210922025631_drop_int4_column_for_ci_sources_pipelines_spec.rb
+++ b/spec/migrations/20210922025631_drop_int4_column_for_ci_sources_pipelines_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration!('drop_int4_column_for_ci_sources_pipelines')
+require_migration!
RSpec.describe DropInt4ColumnForCiSourcesPipelines do
let(:ci_sources_pipelines) { table(:ci_sources_pipelines) }
diff --git a/spec/migrations/20210922082019_drop_int4_column_for_events_spec.rb b/spec/migrations/20210922082019_drop_int4_column_for_events_spec.rb
index 412556fc283..e460612a7d5 100644
--- a/spec/migrations/20210922082019_drop_int4_column_for_events_spec.rb
+++ b/spec/migrations/20210922082019_drop_int4_column_for_events_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration!('drop_int4_column_for_events')
+require_migration!
RSpec.describe DropInt4ColumnForEvents do
let(:events) { table(:events) }
diff --git a/spec/migrations/20210922091402_drop_int4_column_for_push_event_payloads_spec.rb b/spec/migrations/20210922091402_drop_int4_column_for_push_event_payloads_spec.rb
index 2b286e3e5e0..8c89cd19f7f 100644
--- a/spec/migrations/20210922091402_drop_int4_column_for_push_event_payloads_spec.rb
+++ b/spec/migrations/20210922091402_drop_int4_column_for_push_event_payloads_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration!('drop_int4_column_for_push_event_payloads')
+require_migration!
RSpec.describe DropInt4ColumnForPushEventPayloads do
let(:push_event_payloads) { table(:push_event_payloads) }
diff --git a/spec/migrations/20211006060436_schedule_populate_topics_total_projects_count_cache_spec.rb b/spec/migrations/20211006060436_schedule_populate_topics_total_projects_count_cache_spec.rb
index d07d9a71b06..09ce0858b12 100644
--- a/spec/migrations/20211006060436_schedule_populate_topics_total_projects_count_cache_spec.rb
+++ b/spec/migrations/20211006060436_schedule_populate_topics_total_projects_count_cache_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration!('schedule_populate_topics_total_projects_count_cache')
+require_migration!
RSpec.describe SchedulePopulateTopicsTotalProjectsCountCache do
let(:topics) { table(:topics) }
diff --git a/spec/migrations/20211012134316_clean_up_migrate_merge_request_diff_commit_users_spec.rb b/spec/migrations/20211012134316_clean_up_migrate_merge_request_diff_commit_users_spec.rb
new file mode 100644
index 00000000000..910e6d1d91b
--- /dev/null
+++ b/spec/migrations/20211012134316_clean_up_migrate_merge_request_diff_commit_users_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration! 'clean_up_migrate_merge_request_diff_commit_users'
+
+RSpec.describe CleanUpMigrateMergeRequestDiffCommitUsers, :migration do
+ describe '#up' do
+ context 'when there are pending jobs' do
+ it 'processes the jobs immediately' do
+ Gitlab::Database::BackgroundMigrationJob.create!(
+ class_name: 'MigrateMergeRequestDiffCommitUsers',
+ status: :pending,
+ arguments: [10, 20]
+ )
+
+ spy = Gitlab::BackgroundMigration::MigrateMergeRequestDiffCommitUsers
+ migration = described_class.new
+
+ allow(Gitlab::BackgroundMigration::MigrateMergeRequestDiffCommitUsers)
+ .to receive(:new)
+ .and_return(spy)
+
+ expect(migration).to receive(:say)
+ expect(spy).to receive(:perform).with(10, 20)
+
+ migration.up
+ end
+ end
+
+ context 'when all jobs are completed' do
+ it 'does nothing' do
+ Gitlab::Database::BackgroundMigrationJob.create!(
+ class_name: 'MigrateMergeRequestDiffCommitUsers',
+ status: :succeeded,
+ arguments: [10, 20]
+ )
+
+ migration = described_class.new
+
+ expect(migration).not_to receive(:say)
+ expect(Gitlab::BackgroundMigration::MigrateMergeRequestDiffCommitUsers)
+ .not_to receive(:new)
+
+ migration.up
+ end
+ end
+ end
+end
diff --git a/spec/migrations/20201112130710_schedule_remove_duplicate_vulnerabilities_findings_spec.rb b/spec/migrations/20211018152654_schedule_remove_duplicate_vulnerabilities_findings3_spec.rb
index 92a716c355b..95c5be2fc30 100644
--- a/spec/migrations/20201112130710_schedule_remove_duplicate_vulnerabilities_findings_spec.rb
+++ b/spec/migrations/20211018152654_schedule_remove_duplicate_vulnerabilities_findings3_spec.rb
@@ -1,14 +1,14 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration!('schedule_remove_duplicate_vulnerabilities_findings')
+require_migration!('schedule_remove_duplicate_vulnerabilities_findings3')
-RSpec.describe ScheduleRemoveDuplicateVulnerabilitiesFindings, :migration do
+RSpec.describe ScheduleRemoveDuplicateVulnerabilitiesFindings3, :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(:project) { table(:projects).create!(id: 14219619, 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!(:scanner1) { scanners.create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
let!(:scanner2) { scanners.create!(project_id: project.id, external_id: 'test 2', name: 'test scanner 2') }
let!(:scanner3) { scanners.create!(project_id: project.id, external_id: 'test 3', name: 'test scanner 3') }
let!(:unrelated_scanner) { scanners.create!(project_id: project.id, external_id: 'unreleated_scanner', name: 'unrelated scanner') }
@@ -17,43 +17,68 @@ RSpec.describe ScheduleRemoveDuplicateVulnerabilitiesFindings, :migration do
let(:vulnerability_identifiers) { table(:vulnerability_identifiers) }
let(:vulnerability_identifier) do
vulnerability_identifiers.create!(
+ id: 1244459,
project_id: project.id,
external_type: 'vulnerability-identifier',
external_id: 'vulnerability-identifier',
- fingerprint: '7e394d1b1eb461a7406d7b1e08f057a1cf11287a',
+ fingerprint: '0a203e8cd5260a1948edbedc76c7cb91ad6a2e45',
name: 'vulnerability identifier')
end
- let!(:first_finding) do
+ let!(:vulnerability_for_first_duplicate) do
+ create_vulnerability!(
+ project_id: project.id,
+ author_id: user.id
+ )
+ end
+
+ let!(:first_finding_duplicate) do
create_finding!(
- uuid: "test1",
- vulnerability_id: nil,
+ id: 5606961,
+ uuid: "bd95c085-71aa-51d7-9bb6-08ae669c262e",
+ vulnerability_id: vulnerability_for_first_duplicate.id,
report_type: 0,
- location_fingerprint: '2bda3014914481791847d8eca38d1a8d13b6ad76',
+ location_fingerprint: '00049d5119c2cb3bfb3d1ee1f6e031fe925aed75',
primary_identifier_id: vulnerability_identifier.id,
- scanner_id: scanner.id,
+ scanner_id: scanner1.id,
project_id: project.id
)
end
- let!(:first_duplicate) do
+ let!(:vulnerability_for_second_duplicate) do
+ create_vulnerability!(
+ project_id: project.id,
+ author_id: user.id
+ )
+ end
+
+ let!(:second_finding_duplicate) do
create_finding!(
- uuid: "test2",
- vulnerability_id: nil,
+ id: 8765432,
+ uuid: "5b714f58-1176-5b26-8fd5-e11dfcb031b5",
+ vulnerability_id: vulnerability_for_second_duplicate.id,
report_type: 0,
- location_fingerprint: '2bda3014914481791847d8eca38d1a8d13b6ad76',
+ location_fingerprint: '00049d5119c2cb3bfb3d1ee1f6e031fe925aed75',
primary_identifier_id: vulnerability_identifier.id,
scanner_id: scanner2.id,
project_id: project.id
)
end
- let!(:second_duplicate) do
+ let!(:vulnerability_for_third_duplicate) do
+ create_vulnerability!(
+ project_id: project.id,
+ author_id: user.id
+ )
+ end
+
+ let!(:third_finding_duplicate) do
create_finding!(
- uuid: "test3",
- vulnerability_id: nil,
+ id: 8832995,
+ uuid: "cfe435fa-b25b-5199-a56d-7b007cc9e2d4",
+ vulnerability_id: vulnerability_for_third_duplicate.id,
report_type: 0,
- location_fingerprint: '2bda3014914481791847d8eca38d1a8d13b6ad76',
+ location_fingerprint: '00049d5119c2cb3bfb3d1ee1f6e031fe925aed75',
primary_identifier_id: vulnerability_identifier.id,
scanner_id: scanner3.id,
project_id: project.id
@@ -62,6 +87,7 @@ RSpec.describe ScheduleRemoveDuplicateVulnerabilitiesFindings, :migration do
let!(:unrelated_finding) do
create_finding!(
+ id: 9999999,
uuid: "unreleated_finding",
vulnerability_id: nil,
report_type: 1,
@@ -84,9 +110,9 @@ RSpec.describe ScheduleRemoveDuplicateVulnerabilitiesFindings, :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(first_finding_duplicate.id, first_finding_duplicate.id)
+ expect(described_class::MIGRATION).to be_scheduled_migration(second_finding_duplicate.id, second_finding_duplicate.id)
+ expect(described_class::MIGRATION).to be_scheduled_migration(third_finding_duplicate.id, third_finding_duplicate.id)
expect(described_class::MIGRATION).to be_scheduled_migration(unrelated_finding.id, unrelated_finding.id)
end
@@ -105,11 +131,13 @@ RSpec.describe ScheduleRemoveDuplicateVulnerabilitiesFindings, :migration do
# rubocop:disable Metrics/ParameterLists
def create_finding!(
+ id: nil,
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_findings.create!({
+ id: id,
vulnerability_id: vulnerability_id,
project_id: project_id,
name: name,
@@ -123,11 +151,11 @@ RSpec.describe ScheduleRemoveDuplicateVulnerabilitiesFindings, :migration do
metadata_version: metadata_version,
raw_metadata: raw_metadata,
uuid: uuid
- )
+ }.compact)
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)
+ 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,
diff --git a/spec/migrations/20211028155449_schedule_fix_merge_request_diff_commit_users_migration_spec.rb b/spec/migrations/20211028155449_schedule_fix_merge_request_diff_commit_users_migration_spec.rb
new file mode 100644
index 00000000000..6511f554436
--- /dev/null
+++ b/spec/migrations/20211028155449_schedule_fix_merge_request_diff_commit_users_migration_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration! 'schedule_fix_merge_request_diff_commit_users_migration'
+
+RSpec.describe ScheduleFixMergeRequestDiffCommitUsersMigration, :migration do
+ let(:migration) { described_class.new }
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:namespace) { namespaces.create!(name: 'foo', path: 'foo') }
+
+ describe '#up' do
+ it 'does nothing when there are no projects to correct' do
+ migration.up
+
+ expect(Gitlab::Database::BackgroundMigrationJob.count).to be_zero
+ end
+
+ it 'schedules imported projects created after July' do
+ project = projects.create!(
+ namespace_id: namespace.id,
+ import_type: 'gitlab_project',
+ created_at: '2021-08-01'
+ )
+
+ expect(migration)
+ .to receive(:migrate_in)
+ .with(2.minutes, 'FixMergeRequestDiffCommitUsers', [project.id])
+
+ migration.up
+
+ expect(Gitlab::Database::BackgroundMigrationJob.count).to eq(1)
+
+ job = Gitlab::Database::BackgroundMigrationJob.first
+
+ expect(job.class_name).to eq('FixMergeRequestDiffCommitUsers')
+ expect(job.arguments).to eq([project.id])
+ end
+
+ it 'ignores projects imported before July' do
+ projects.create!(
+ namespace_id: namespace.id,
+ import_type: 'gitlab_project',
+ created_at: '2020-08-01'
+ )
+
+ migration.up
+
+ expect(Gitlab::Database::BackgroundMigrationJob.count).to be_zero
+ end
+
+ it 'ignores projects that are not imported' do
+ projects.create!(
+ namespace_id: namespace.id,
+ created_at: '2021-08-01'
+ )
+
+ migration.up
+
+ expect(Gitlab::Database::BackgroundMigrationJob.count).to be_zero
+ end
+ end
+end
diff --git a/spec/migrations/add_default_value_stream_to_groups_with_group_stages_spec.rb b/spec/migrations/add_default_value_stream_to_groups_with_group_stages_spec.rb
deleted file mode 100644
index f21acbc56df..00000000000
--- a/spec/migrations/add_default_value_stream_to_groups_with_group_stages_spec.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe AddDefaultValueStreamToGroupsWithGroupStages, schema: 20200624142207 do
- let(:groups) { table(:namespaces) }
- let(:group_stages) { table(:analytics_cycle_analytics_group_stages) }
- let(:value_streams) { table(:analytics_cycle_analytics_group_value_streams) }
-
- let!(:group) { groups.create!(name: 'test', path: 'path', type: 'Group') }
- let!(:group_stage) { group_stages.create!(name: 'test', group_id: group.id, start_event_identifier: 1, end_event_identifier: 2) }
-
- describe '#up' do
- it 'creates default value stream record for the group' do
- migrate!
-
- group_value_streams = value_streams.where(group_id: group.id)
- expect(group_value_streams.size).to eq(1)
-
- value_stream = group_value_streams.first
- expect(value_stream.name).to eq('default')
- end
-
- it 'migrates existing stages to the default value stream' do
- migrate!
-
- group_stage.reload
-
- value_stream = value_streams.find_by(group_id: group.id, name: 'default')
- expect(group_stage.group_value_stream_id).to eq(value_stream.id)
- end
- end
-
- describe '#down' do
- it 'sets the group_value_stream_id to nil' do
- described_class.new.down
-
- group_stage.reload
-
- expect(group_stage.group_value_stream_id).to be_nil
- end
- end
-end
diff --git a/spec/migrations/add_deploy_token_type_to_deploy_tokens_spec.rb b/spec/migrations/add_deploy_token_type_to_deploy_tokens_spec.rb
deleted file mode 100644
index f90bfcd313c..00000000000
--- a/spec/migrations/add_deploy_token_type_to_deploy_tokens_spec.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe AddDeployTokenTypeToDeployTokens do
- let(:deploy_tokens) { table(:deploy_tokens) }
- let(:deploy_token) do
- deploy_tokens.create!(name: 'token_test',
- username: 'gitlab+deploy-token-1',
- token_encrypted: 'dr8rPXwM+Mbs2p3Bg1+gpnXqrnH/wu6vaHdcc7A3isPR67WB',
- read_repository: true,
- expires_at: Time.now + 1.year)
- end
-
- it 'updates the deploy_token_type column to 2' do
- expect(deploy_token).not_to respond_to(:deploy_token_type)
-
- migrate!
-
- deploy_token.reload
- expect(deploy_token.deploy_token_type).to eq(2)
- end
-end
diff --git a/spec/migrations/add_incident_settings_to_all_existing_projects_spec.rb b/spec/migrations/add_incident_settings_to_all_existing_projects_spec.rb
deleted file mode 100644
index 3e0bc64bb23..00000000000
--- a/spec/migrations/add_incident_settings_to_all_existing_projects_spec.rb
+++ /dev/null
@@ -1,93 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe AddIncidentSettingsToAllExistingProjects, :migration do
- let(:project_incident_management_settings) { table(:project_incident_management_settings) }
- let(:labels) { table(:labels) }
- let(:label_links) { table(:label_links) }
- let(:issues) { table(:issues) }
- let(:projects) { table(:projects) }
- let(:namespaces) { table(:namespaces) }
-
- RSpec.shared_examples 'setting not added' do
- it 'does not add settings' do
- migrate!
-
- expect { migrate! }.not_to change { IncidentManagement::ProjectIncidentManagementSetting.count }
- end
- end
-
- RSpec.shared_examples 'project has no incident settings' do
- it 'has no settings' do
- migrate!
-
- expect(settings).to eq(nil)
- end
- end
-
- RSpec.shared_examples 'no change to incident settings' do
- it 'does not change existing settings' do
- migrate!
-
- expect(settings.create_issue).to eq(existing_create_issue)
- end
- end
-
- RSpec.shared_context 'with incident settings' do
- let(:existing_create_issue) { false }
- before do
- project_incident_management_settings.create!(
- project_id: project.id,
- create_issue: existing_create_issue
- )
- end
- end
-
- describe 'migrate!' do
- let(:namespace) { namespaces.create!(name: 'foo', path: 'foo') }
- let!(:project) { projects.create!(namespace_id: namespace.id) }
- let(:settings) { project_incident_management_settings.find_by(project_id: project.id) }
-
- context 'when project does not have incident label' do
- context 'does not have incident settings' do
- include_examples 'setting not added'
- include_examples 'project has no incident settings'
- end
-
- context 'and has incident settings' do
- include_context 'with incident settings'
-
- include_examples 'setting not added'
- include_examples 'no change to incident settings'
- end
- end
-
- context 'when project has incident labels' do
- before do
- issue = issues.create!(project_id: project.id)
- incident_label_attrs = IncidentManagement::CreateIncidentLabelService::LABEL_PROPERTIES
- incident_label = labels.create!(project_id: project.id, **incident_label_attrs)
- label_links.create!(target_id: issue.id, label_id: incident_label.id, target_type: 'Issue')
- end
-
- context 'when project has incident settings' do
- include_context 'with incident settings'
-
- include_examples 'setting not added'
- include_examples 'no change to incident settings'
- end
-
- context 'does not have incident settings' do
- it 'adds incident settings with old defaults' do
- migrate!
-
- expect(settings.create_issue).to eq(true)
- expect(settings.send_email).to eq(false)
- expect(settings.issue_template_key).to eq(nil)
- end
- end
- end
- end
-end
diff --git a/spec/migrations/add_open_source_plan_spec.rb b/spec/migrations/add_open_source_plan_spec.rb
new file mode 100644
index 00000000000..04b26662f82
--- /dev/null
+++ b/spec/migrations/add_open_source_plan_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe AddOpenSourcePlan, :migration do
+ describe '#up' do
+ before do
+ allow(Gitlab).to receive(:dev_env_or_com?).and_return true
+ end
+
+ it 'creates 1 entry within the plans table' do
+ expect { migrate! }.to change { AddOpenSourcePlan::Plan.count }.by 1
+ expect(AddOpenSourcePlan::Plan.last.name).to eql('opensource')
+ end
+
+ it 'creates 1 entry for plan limits' do
+ expect { migrate! }.to change { AddOpenSourcePlan::PlanLimits.count }.by 1
+ end
+
+ context 'when the plan limits for gold and silver exists' do
+ before do
+ table(:plans).create!(id: 1, name: 'ultimate', title: 'Ultimate')
+ table(:plan_limits).create!(id: 1, plan_id: 1, storage_size_limit: 2000)
+ end
+
+ it 'duplicates the gold and silvers plan limits entries' do
+ migrate!
+
+ opensource_limits = AddOpenSourcePlan::Plan.find_by(name: 'opensource').limits
+ expect(opensource_limits.storage_size_limit).to be 2000
+ end
+ end
+
+ context 'when the instance is not SaaS' do
+ before do
+ allow(Gitlab).to receive(:dev_env_or_com?).and_return false
+ end
+
+ it 'does not create plans and plan limits and returns' do
+ expect { migrate! }.not_to change { AddOpenSourcePlan::Plan.count }
+ end
+ end
+ end
+
+ describe '#down' do
+ before do
+ table(:plans).create!(id: 3, name: 'other')
+ table(:plan_limits).create!(plan_id: 3)
+ end
+
+ context 'when the instance is SaaS' do
+ before do
+ allow(Gitlab).to receive(:dev_env_or_com?).and_return true
+ end
+
+ it 'removes the newly added opensource entry' do
+ migrate!
+
+ expect { described_class.new.down }.to change { AddOpenSourcePlan::Plan.count }.by(-1)
+ expect(AddOpenSourcePlan::Plan.find_by(name: 'opensource')).to be_nil
+
+ other_plan = AddOpenSourcePlan::Plan.find_by(name: 'other')
+ expect(other_plan).to be_persisted
+ expect(AddOpenSourcePlan::PlanLimits.count).to eq(1)
+ expect(AddOpenSourcePlan::PlanLimits.first.plan_id).to eq(other_plan.id)
+ end
+ end
+
+ context 'when the instance is not SaaS' do
+ before do
+ allow(Gitlab).to receive(:dev_env_or_com?).and_return false
+ table(:plans).create!(id: 1, name: 'opensource', title: 'Open Source Program')
+ table(:plan_limits).create!(id: 1, plan_id: 1)
+ end
+
+ it 'does not delete plans and plan limits and returns' do
+ migrate!
+
+ expect { described_class.new.down }.not_to change { AddOpenSourcePlan::Plan.count }
+ expect(AddOpenSourcePlan::PlanLimits.count).to eq(2)
+ end
+ end
+ end
+end
diff --git a/spec/migrations/add_partial_index_to_ci_builds_table_on_user_id_name_spec.rb b/spec/migrations/add_partial_index_to_ci_builds_table_on_user_id_name_spec.rb
deleted file mode 100644
index ab4d6f43797..00000000000
--- a/spec/migrations/add_partial_index_to_ci_builds_table_on_user_id_name_spec.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe AddPartialIndexToCiBuildsTableOnUserIdName do
- let(:migration) { described_class.new }
-
- describe '#up' do
- it 'creates temporary partial index on type' do
- expect { migration.up }.to change { migration.index_exists?(:ci_builds, [:user_id, :name], name: described_class::INDEX_NAME) }.from(false).to(true)
- end
- end
-
- describe '#down' do
- it 'removes temporary partial index on type' do
- migration.up
-
- expect { migration.down }.to change { migration.index_exists?(:ci_builds, [:user_id, :name], name: described_class::INDEX_NAME) }.from(true).to(false)
- end
- end
-end
diff --git a/spec/migrations/add_repository_storages_weighted_to_application_settings_spec.rb b/spec/migrations/add_repository_storages_weighted_to_application_settings_spec.rb
deleted file mode 100644
index bc4c510fea3..00000000000
--- a/spec/migrations/add_repository_storages_weighted_to_application_settings_spec.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe AddRepositoryStoragesWeightedToApplicationSettings, :migration do
- let(:storages) { { "foo" => {}, "baz" => {} } }
- let(:application_settings) do
- table(:application_settings).tap do |klass|
- klass.class_eval do
- serialize :repository_storages
- end
- end
- end
-
- before do
- allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
- end
-
- let(:application_setting) { application_settings.create! }
- let(:repository_storages) { ["foo"] }
-
- it 'populates repository_storages_weighted properly' do
- application_setting.repository_storages = repository_storages
- application_setting.save!
-
- migrate!
-
- expect(application_settings.find(application_setting.id).repository_storages_weighted).to eq({ "foo" => 100, "baz" => 0 })
- end
-end
diff --git a/spec/migrations/add_temporary_partial_index_on_project_id_to_services_spec.rb b/spec/migrations/add_temporary_partial_index_on_project_id_to_services_spec.rb
deleted file mode 100644
index dae0241b895..00000000000
--- a/spec/migrations/add_temporary_partial_index_on_project_id_to_services_spec.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe AddTemporaryPartialIndexOnProjectIdToServices do
- let(:migration) { described_class.new }
-
- describe '#up' do
- it 'creates temporary partial index on type' do
- expect { migration.up }.to change { migration.index_exists?(:services, :project_id, name: described_class::INDEX_NAME) }.from(false).to(true)
- end
- end
-
- describe '#down' do
- it 'removes temporary partial index on type' do
- migration.up
-
- expect { migration.down }.to change { migration.index_exists?(:services, :project_id, name: described_class::INDEX_NAME) }.from(true).to(false)
- end
- end
-end
diff --git a/spec/migrations/backfill_imported_snippet_repositories_spec.rb b/spec/migrations/backfill_imported_snippet_repositories_spec.rb
deleted file mode 100644
index 7052433c66d..00000000000
--- a/spec/migrations/backfill_imported_snippet_repositories_spec.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe BackfillImportedSnippetRepositories do
- let(:users) { table(:users) }
- let(:snippets) { table(:snippets) }
- let(:user) { users.create!(id: 1, email: 'user@example.com', projects_limit: 10, username: 'test', name: 'Test', state: 'active') }
-
- def create_snippet(id)
- params = {
- id: id,
- type: 'PersonalSnippet',
- author_id: user.id,
- file_name: 'foo',
- content: 'bar'
- }
-
- snippets.create!(params)
- end
-
- it 'correctly schedules background migrations' do
- create_snippet(1)
- create_snippet(2)
- create_snippet(3)
- create_snippet(5)
- create_snippet(7)
- create_snippet(8)
- create_snippet(10)
-
- 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, 5, 5)
-
- expect(described_class::MIGRATION)
- .to be_scheduled_delayed_migration(6.minutes, 7, 8)
-
- expect(described_class::MIGRATION)
- .to be_scheduled_delayed_migration(8.minutes, 10, 10)
-
- expect(BackgroundMigrationWorker.jobs.size).to eq(4)
- end
- end
- end
-end
diff --git a/spec/migrations/backfill_operations_feature_flags_iid_spec.rb b/spec/migrations/backfill_operations_feature_flags_iid_spec.rb
deleted file mode 100644
index 3c400840f98..00000000000
--- a/spec/migrations/backfill_operations_feature_flags_iid_spec.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe BackfillOperationsFeatureFlagsIid do
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:flags) { table(:operations_feature_flags) }
-
- def setup
- namespace = namespaces.create!(name: 'foo', path: 'foo')
- projects.create!(namespace_id: namespace.id)
- end
-
- it 'migrates successfully when there are no flags in the database' do
- setup
-
- disable_migrations_output { migrate! }
-
- expect(flags.count).to eq(0)
- end
-
- it 'migrates successfully with a row in the table in both FOSS and EE' do
- project = setup
- flags.create!(project_id: project.id, active: true, name: 'test_flag')
-
- disable_migrations_output { migrate! }
-
- expect(flags.count).to eq(1)
- end
-end
diff --git a/spec/migrations/backfill_snippet_repositories_spec.rb b/spec/migrations/backfill_snippet_repositories_spec.rb
deleted file mode 100644
index 64cfc9cc57b..00000000000
--- a/spec/migrations/backfill_snippet_repositories_spec.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe BackfillSnippetRepositories do
- let(:users) { table(:users) }
- let(:snippets) { table(:snippets) }
- let(:user) { users.create!(id: 1, email: 'user@example.com', projects_limit: 10, username: 'test', name: 'Test', state: 'active') }
-
- def create_snippet(id)
- params = {
- id: id,
- type: 'PersonalSnippet',
- author_id: user.id,
- file_name: 'foo',
- content: 'bar'
- }
-
- snippets.create!(params)
- end
-
- it 'correctly schedules background migrations' do
- create_snippet(1)
- create_snippet(2)
- create_snippet(3)
-
- 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(3.minutes, 1, 2)
-
- expect(described_class::MIGRATION)
- .to be_scheduled_delayed_migration(6.minutes, 3, 3)
-
- expect(BackgroundMigrationWorker.jobs.size).to eq(2)
- end
- end
- end
-end
diff --git a/spec/migrations/backfill_status_page_published_incidents_spec.rb b/spec/migrations/backfill_status_page_published_incidents_spec.rb
deleted file mode 100644
index fa4bb182362..00000000000
--- a/spec/migrations/backfill_status_page_published_incidents_spec.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe BackfillStatusPagePublishedIncidents, :migration do
- subject(:migration) { described_class.new }
-
- describe '#up' do
- let(:projects) { table(:projects) }
- let(:status_page_settings) { table(:status_page_settings) }
- let(:issues) { table(:issues) }
- let(:incidents) { table(:status_page_published_incidents) }
-
- let(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab') }
- let(:project_without_status_page) { projects.create!(namespace_id: namespace.id) }
- let(:enabled_project) { projects.create!(namespace_id: namespace.id) }
- let(:disabled_project) { projects.create!(namespace_id: namespace.id) }
-
- let!(:enabled_setting) { status_page_settings.create!(enabled: true, project_id: enabled_project.id, **status_page_setting_attrs) }
- let!(:disabled_setting) { status_page_settings.create!(enabled: false, project_id: disabled_project.id, **status_page_setting_attrs) }
-
- let!(:published_issue) { issues.create!(confidential: false, project_id: enabled_project.id) }
- let!(:nonpublished_issue_1) { issues.create!(confidential: true, project_id: enabled_project.id) }
- let!(:nonpublished_issue_2) { issues.create!(confidential: false, project_id: disabled_project.id) }
- let!(:nonpublished_issue_3) { issues.create!(confidential: false, project_id: project_without_status_page.id) }
-
- let(:current_time) { Time.current.change(usec: 0) }
- let(:status_page_setting_attrs) do
- {
- aws_s3_bucket_name: 'bucket',
- aws_region: 'region',
- aws_access_key: 'key',
- encrypted_aws_secret_key: 'abc123',
- encrypted_aws_secret_key_iv: 'abc123'
- }
- end
-
- it 'creates a StatusPage::PublishedIncident record for each published issue' do
- travel_to(current_time) do
- expect(incidents.all).to be_empty
-
- migrate!
-
- incident = incidents.first
-
- expect(incidents.count).to eq(1)
- expect(incident.issue_id).to eq(published_issue.id)
- expect(incident.created_at).to eq(current_time)
- expect(incident.updated_at).to eq(current_time)
- end
- end
- end
-end
diff --git a/spec/migrations/backfill_user_namespace_spec.rb b/spec/migrations/backfill_user_namespace_spec.rb
new file mode 100644
index 00000000000..094aec82e9c
--- /dev/null
+++ b/spec/migrations/backfill_user_namespace_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe BackfillUserNamespace do
+ let_it_be(:migration) { described_class::MIGRATION }
+
+ describe '#up' do
+ it 'schedules background jobs for each batch of namespaces' do
+ migrate!
+
+ expect(migration).to have_scheduled_batched_migration(
+ table_name: :namespaces,
+ column_name: :id,
+ interval: described_class::INTERVAL
+ )
+ end
+ end
+
+ describe '#down' do
+ it 'deletes all batched migration records' do
+ migrate!
+ schema_migrate_down!
+
+ expect(migration).not_to have_scheduled_batched_migration
+ end
+ end
+end
diff --git a/spec/migrations/cap_designs_filename_length_to_new_limit_spec.rb b/spec/migrations/cap_designs_filename_length_to_new_limit_spec.rb
deleted file mode 100644
index 702f2e6d9bd..00000000000
--- a/spec/migrations/cap_designs_filename_length_to_new_limit_spec.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe CapDesignsFilenameLengthToNewLimit, :migration, schema: 20200528125905 do
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:issues) { table(:issues) }
- let(:designs) { table(:design_management_designs) }
-
- let(:filename_below_limit) { generate_filename(254) }
- let(:filename_at_limit) { generate_filename(255) }
- let(:filename_above_limit) { generate_filename(256) }
-
- let!(:namespace) { namespaces.create!(name: 'foo', path: 'foo') }
- let!(:project) { projects.create!(name: 'gitlab', path: 'gitlab-org/gitlab', namespace_id: namespace.id) }
- let!(:issue) { issues.create!(description: 'issue', project_id: project.id) }
-
- def generate_filename(length, extension: '.png')
- name = 'a' * (length - extension.length)
-
- "#{name}#{extension}"
- end
-
- def create_design(filename)
- designs.create!(
- issue_id: issue.id,
- project_id: project.id,
- filename: filename
- )
- end
-
- it 'correctly sets filenames that are above the limit' do
- designs = [
- filename_below_limit,
- filename_at_limit,
- filename_above_limit
- ].map(&method(:create_design))
-
- migrate!
-
- designs.each(&:reload)
-
- expect(designs[0].filename).to eq(filename_below_limit)
- expect(designs[1].filename).to eq(filename_at_limit)
- expect(designs[2].filename).to eq([described_class::MODIFIED_NAME, designs[2].id, described_class::MODIFIED_EXTENSION].join)
- end
-
- it 'runs after filename limit has been set' do
- # This spec file uses the `schema:` keyword to run these tests
- # against a schema version before the one that sets the limit,
- # as otherwise we can't create the design data with filenames greater
- # than the limit.
- #
- # For this test, we migrate any skipped versions up to this migration.
- migration_context.migrate(20200602013901)
-
- create_design(filename_at_limit)
- expect { create_design(filename_above_limit) }.to raise_error(ActiveRecord::StatementInvalid)
- end
-end
diff --git a/spec/migrations/clean_grafana_url_spec.rb b/spec/migrations/clean_grafana_url_spec.rb
deleted file mode 100644
index 7a81eb3058b..00000000000
--- a/spec/migrations/clean_grafana_url_spec.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe CleanGrafanaUrl do
- let(:application_settings_table) { table(:application_settings) }
-
- [
- 'javascript:alert(window.opener.document.location)',
- ' javascript:alert(window.opener.document.location)'
- ].each do |grafana_url|
- it "sets grafana_url back to its default value when grafana_url is '#{grafana_url}'" do
- application_settings = application_settings_table.create!(grafana_url: grafana_url)
-
- migrate!
-
- expect(application_settings.reload.grafana_url).to eq('/-/grafana')
- end
- end
-
- ['/-/grafana', '/some/relative/url', 'http://localhost:9000'].each do |grafana_url|
- it "does not modify grafana_url when grafana_url is '#{grafana_url}'" do
- application_settings = application_settings_table.create!(grafana_url: grafana_url)
-
- migrate!
-
- expect(application_settings.reload.grafana_url).to eq(grafana_url)
- end
- end
-
- context 'when application_settings table has no rows' do
- it 'does not fail' do
- migrate!
- end
- end
-end
diff --git a/spec/migrations/cleanup_empty_commit_user_mentions_spec.rb b/spec/migrations/cleanup_empty_commit_user_mentions_spec.rb
deleted file mode 100644
index d128c13e212..00000000000
--- a/spec/migrations/cleanup_empty_commit_user_mentions_spec.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe CleanupEmptyCommitUserMentions, :migration, :sidekiq do
- let(:users) { table(:users) }
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:notes) { table(:notes) }
-
- let(:user) { users.create!(name: 'root', email: 'root@example.com', username: 'root', projects_limit: 0) }
- let(:group) { namespaces.create!(name: 'group1', path: 'group1', owner_id: user.id) }
- let(:project) { projects.create!(name: 'gitlab1', path: 'gitlab1', namespace_id: group.id, visibility_level: 0) }
-
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') }
- let(:commit) { Commit.new(RepoHelpers.sample_commit, project) }
- let(:commit_user_mentions) { table(:commit_user_mentions) }
-
- let!(:resource1) { notes.create!(commit_id: commit.id, noteable_type: 'Commit', project_id: project.id, author_id: user.id, note: 'note1 for @root to check') }
- let!(:resource2) { notes.create!(commit_id: commit.id, noteable_type: 'Commit', project_id: project.id, author_id: user.id, note: 'note1 for @root to check') }
- let!(:resource3) { notes.create!(commit_id: commit.id, noteable_type: 'Commit', project_id: project.id, author_id: user.id, note: 'note1 for @root to check', system: true) }
-
- # this note is already migrated, as it has a record in the commit_user_mentions table
- let!(:resource4) { notes.create!(note: 'note3 for @root to check', commit_id: commit.id, noteable_type: 'Commit') }
- let!(:user_mention) { commit_user_mentions.create!(commit_id: commit.id, note_id: resource4.id, mentioned_users_ids: [1]) }
-
- # these should get cleanup, by the migration
- let!(:blank_commit_user_mention1) { commit_user_mentions.create!(commit_id: commit.id, note_id: resource1.id)}
- let!(:blank_commit_user_mention2) { commit_user_mentions.create!(commit_id: commit.id, note_id: resource2.id)}
- let!(:blank_commit_user_mention3) { commit_user_mentions.create!(commit_id: commit.id, note_id: resource3.id)}
-
- it 'cleanups blank user mentions' do
- expect { migrate! }.to change { commit_user_mentions.count }.by(-3)
- end
-end
diff --git a/spec/migrations/cleanup_group_import_states_with_null_user_id_spec.rb b/spec/migrations/cleanup_group_import_states_with_null_user_id_spec.rb
deleted file mode 100644
index acd6a19779d..00000000000
--- a/spec/migrations/cleanup_group_import_states_with_null_user_id_spec.rb
+++ /dev/null
@@ -1,101 +0,0 @@
-# frozen_string_literal: true
-
-# In order to test the CleanupGroupImportStatesWithNullUserId migration, we need
-# to first create GroupImportState with NULL user_id
-# and then run the migration to check that user_id was populated or record removed
-#
-# The problem is that the CleanupGroupImportStatesWithNullUserId migration comes
-# after the NOT NULL constraint has been added with a previous migration (AddNotNullConstraintToUserOnGroupImportStates)
-# That means that while testing the current class we can not insert GroupImportState records with an
-# invalid user_id as constraint is blocking it from doing so
-#
-# To solve this problem, use SchemaVersionFinder to set schema one version prior to AddNotNullConstraintToUserOnGroupImportStates
-
-require 'spec_helper'
-require_migration!('add_not_null_constraint_to_user_on_group_import_states')
-require_migration!
-
-RSpec.describe CleanupGroupImportStatesWithNullUserId, :migration,
- schema: MigrationHelpers::SchemaVersionFinder.migration_prior(AddNotNullConstraintToUserOnGroupImportStates) do
- let(:namespaces_table) { table(:namespaces) }
- let(:users_table) { table(:users) }
- let(:group_import_states_table) { table(:group_import_states) }
- let(:members_table) { table(:members) }
-
- describe 'Group import states clean up' do
- context 'when user_id is present' do
- it 'does not update group_import_state record' do
- user_1 = users_table.create!(name: 'user1', email: 'user1@example.com', projects_limit: 1)
- group_1 = namespaces_table.create!(name: 'group_1', path: 'group_1', type: 'Group')
- create_member(user_id: user_1.id, type: 'GroupMember', source_type: 'Namespace', source_id: group_1.id, access_level: described_class::Group::OWNER)
- group_import_state_1 = group_import_states_table.create!(group_id: group_1.id, user_id: user_1.id, status: 0)
-
- expect(group_import_state_1.user_id).to eq(user_1.id)
-
- disable_migrations_output { migrate! }
-
- expect(group_import_state_1.reload.user_id).to eq(user_1.id)
- end
- end
-
- context 'when user_id is missing' do
- it 'updates user_id with group default owner id' do
- user_2 = users_table.create!(name: 'user2', email: 'user2@example.com', projects_limit: 1)
- group_2 = namespaces_table.create!(name: 'group_2', path: 'group_2', type: 'Group')
- create_member(user_id: user_2.id, type: 'GroupMember', source_type: 'Namespace', source_id: group_2.id, access_level: described_class::Group::OWNER)
- group_import_state_2 = group_import_states_table.create!(group_id: group_2.id, user_id: nil, status: 0)
-
- disable_migrations_output { migrate! }
-
- expect(group_import_state_2.reload.user_id).to eq(user_2.id)
- end
- end
-
- context 'when group does not contain any owners' do
- it 'removes group_import_state record' do
- group_3 = namespaces_table.create!(name: 'group_3', path: 'group_3', type: 'Group')
- group_import_state_3 = group_import_states_table.create!(group_id: group_3.id, user_id: nil, status: 0)
-
- disable_migrations_output { migrate! }
-
- expect { group_import_state_3.reload }.to raise_error(ActiveRecord::RecordNotFound)
- end
- end
-
- context 'when group has parent' do
- it 'updates user_id with parent group default owner id' do
- user = users_table.create!(name: 'user4', email: 'user4@example.com', projects_limit: 1)
- group_1 = namespaces_table.create!(name: 'group_1', path: 'group_1', type: 'Group')
- create_member(user_id: user.id, type: 'GroupMember', source_type: 'Namespace', source_id: group_1.id, access_level: described_class::Group::OWNER)
- group_2 = namespaces_table.create!(name: 'group_2', path: 'group_2', type: 'Group', parent_id: group_1.id)
- group_import_state = group_import_states_table.create!(group_id: group_2.id, user_id: nil, status: 0)
-
- disable_migrations_output { migrate! }
-
- expect(group_import_state.reload.user_id).to eq(user.id)
- end
- end
-
- context 'when group has owner_id' do
- it 'updates user_id with owner_id' do
- user = users_table.create!(name: 'user', email: 'user@example.com', projects_limit: 1)
- group = namespaces_table.create!(name: 'group', path: 'group', type: 'Group', owner_id: user.id)
- group_import_state = group_import_states_table.create!(group_id: group.id, user_id: nil, status: 0)
-
- disable_migrations_output { migrate! }
-
- expect(group_import_state.reload.user_id).to eq(user.id)
- end
- end
- end
-
- def create_member(options)
- members_table.create!(
- {
- notification_level: 0,
- ldap: false,
- override: false
- }.merge(options)
- )
- end
-end
diff --git a/spec/migrations/cleanup_move_container_registry_enabled_to_project_features_spec.rb b/spec/migrations/cleanup_move_container_registry_enabled_to_project_feature_spec.rb
index 3c39327304e..f0f9249515b 100644
--- a/spec/migrations/cleanup_move_container_registry_enabled_to_project_features_spec.rb
+++ b/spec/migrations/cleanup_move_container_registry_enabled_to_project_feature_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration!('cleanup_move_container_registry_enabled_to_project_feature')
+require_migration!
RSpec.describe CleanupMoveContainerRegistryEnabledToProjectFeature, :migration do
let(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab-org') }
diff --git a/spec/migrations/cleanup_optimistic_locking_nulls_pt2_fixed_spec.rb b/spec/migrations/cleanup_optimistic_locking_nulls_pt2_fixed_spec.rb
deleted file mode 100644
index 2f461ebc1d5..00000000000
--- a/spec/migrations/cleanup_optimistic_locking_nulls_pt2_fixed_spec.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!('cleanup_optimistic_locking_nulls_pt2_fixed')
-
-RSpec.describe CleanupOptimisticLockingNullsPt2Fixed, :migration, schema: 20200219193117 do
- test_tables = %w(ci_stages ci_builds ci_pipelines).freeze
- test_tables.each do |table|
- let(table.to_sym) { table(table.to_sym) }
- end
- let(:tables) { test_tables.map { |t| method(t.to_sym).call } }
-
- before do
- # Create necessary rows
- ci_stages.create!
- ci_builds.create!
- ci_pipelines.create!
-
- # Nullify `lock_version` column for all rows
- # Needs to be done with a SQL fragment, otherwise Rails will coerce it to 0
- tables.each do |table|
- table.update_all('lock_version = NULL')
- end
- end
-
- it 'correctly migrates nullified lock_version column', :sidekiq_might_not_need_inline do
- tables.each do |table|
- expect(table.where(lock_version: nil).count).to eq(1)
- end
-
- tables.each do |table|
- expect(table.where(lock_version: 0).count).to eq(0)
- end
-
- migrate!
-
- tables.each do |table|
- expect(table.where(lock_version: nil).count).to eq(0)
- end
-
- tables.each do |table|
- expect(table.where(lock_version: 0).count).to eq(1)
- end
- end
-end
diff --git a/spec/migrations/cleanup_optimistic_locking_nulls_spec.rb b/spec/migrations/cleanup_optimistic_locking_nulls_spec.rb
deleted file mode 100644
index a287d950c89..00000000000
--- a/spec/migrations/cleanup_optimistic_locking_nulls_spec.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!('cleanup_optimistic_locking_nulls')
-
-RSpec.describe CleanupOptimisticLockingNulls do
- let(:epics) { table(:epics) }
- let(:merge_requests) { table(:merge_requests) }
- let(:issues) { table(:issues) }
- let(:tables) { [epics, merge_requests, issues] }
-
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:users) { table(:users)}
-
- before do
- namespaces.create!(id: 123, name: 'gitlab1', path: 'gitlab1')
- projects.create!(id: 123, name: 'gitlab1', path: 'gitlab1', namespace_id: 123)
- users.create!(id: 123, username: 'author', projects_limit: 1000)
-
- # Create necessary rows
- epics.create!(iid: 123, group_id: 123, author_id: 123, title: 'a', title_html: 'a')
- merge_requests.create!(iid: 123, target_project_id: 123, source_project_id: 123, target_branch: 'master', source_branch: 'hmm', title: 'a', title_html: 'a')
- issues.create!(iid: 123, project_id: 123, title: 'a', title_html: 'a')
-
- # Nullify `lock_version` column for all rows
- # Needs to be done with a SQL fragment, otherwise Rails will coerce it to 0
- tables.each do |table|
- table.update_all('lock_version = NULL')
- end
- end
-
- it 'correctly migrates nullified lock_version column', :sidekiq_inline do
- tables.each do |table|
- expect(table.where(lock_version: nil).count).to eq(1)
- end
-
- tables.each do |table|
- expect(table.where(lock_version: 0).count).to eq(0)
- end
-
- migrate!
-
- tables.each do |table|
- expect(table.where(lock_version: nil).count).to eq(0)
- end
-
- tables.each do |table|
- expect(table.where(lock_version: 0).count).to eq(1)
- end
- end
-end
diff --git a/spec/migrations/cleanup_projects_with_missing_namespace_spec.rb b/spec/migrations/cleanup_projects_with_missing_namespace_spec.rb
deleted file mode 100644
index c640bfcd174..00000000000
--- a/spec/migrations/cleanup_projects_with_missing_namespace_spec.rb
+++ /dev/null
@@ -1,142 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!('add_projects_foreign_key_to_namespaces')
-require_migration!
-
-# In order to test the CleanupProjectsWithMissingNamespace migration, we need
-# to first create an orphaned project (one with an invalid namespace_id)
-# and then run the migration to check that the project was properly cleaned up
-#
-# The problem is that the CleanupProjectsWithMissingNamespace migration comes
-# after the FK has been added with a previous migration (AddProjectsForeignKeyToNamespaces)
-# That means that while testing the current class we can not insert projects with an
-# invalid namespace_id as the existing FK is correctly blocking us from doing so
-#
-# The approach that solves that problem is to:
-# - Set the schema of this test to the one prior to AddProjectsForeignKeyToNamespaces
-# - We could hardcode it to `20200508091106` (which currently is the previous
-# migration before adding the FK) but that would mean that this test depends
-# on migration 20200508091106 not being reverted or deleted
-# - So, we use SchemaVersionFinder that finds the previous migration and returns
-# its schema, which we then use in the describe
-#
-# That means that we lock the schema version to the one returned by
-# SchemaVersionFinder.previous_migration and only test the cleanup migration
-# *without* the migration that adds the Foreign Key ever running
-# That's acceptable as the cleanup script should not be affected in any way
-# by the migration that adds the Foreign Key
-class SchemaVersionFinder
- def self.migrations_paths
- ActiveRecord::Migrator.migrations_paths
- end
-
- def self.migration_context
- ActiveRecord::MigrationContext.new(migrations_paths, ActiveRecord::SchemaMigration)
- end
-
- def self.migrations
- migration_context.migrations
- end
-
- def self.previous_migration
- migrations.each_cons(2) do |previous, migration|
- break previous.version if migration.name == AddProjectsForeignKeyToNamespaces.name
- end
- end
-end
-
-RSpec.describe CleanupProjectsWithMissingNamespace, :migration, schema: SchemaVersionFinder.previous_migration do
- let(:projects) { table(:projects) }
- let(:namespaces) { table(:namespaces) }
- let(:users) { table(:users) }
-
- before do
- namespace = namespaces.create!(name: 'existing_namespace', path: 'existing_namespace')
-
- projects.create!(
- name: 'project_with_existing_namespace',
- path: 'project_with_existing_namespace',
- visibility_level: 20,
- archived: false,
- namespace_id: namespace.id
- )
-
- projects.create!(
- name: 'project_with_non_existing_namespace',
- path: 'project_with_non_existing_namespace',
- visibility_level: 20,
- archived: false,
- namespace_id: non_existing_record_id
- )
- end
-
- it 'creates the ghost user' do
- expect(users.where(user_type: described_class::User::USER_TYPE_GHOST).count).to eq(0)
-
- disable_migrations_output { migrate! }
-
- expect(users.where(user_type: described_class::User::USER_TYPE_GHOST).count).to eq(1)
- end
-
- it 'creates the lost-and-found group, owned by the ghost user' do
- expect(
- described_class::Group.where(
- described_class::Group
- .arel_table[:name]
- .matches("#{described_class::User::LOST_AND_FOUND_GROUP}%")
- ).count
- ).to eq(0)
-
- disable_migrations_output { migrate! }
-
- ghost_user = users.find_by(user_type: described_class::User::USER_TYPE_GHOST)
- expect(
- described_class::Group
- .joins('INNER JOIN members ON namespaces.id = members.source_id')
- .where(namespaces: { type: 'Group' })
- .where(members: { type: 'GroupMember' })
- .where(members: { source_type: 'Namespace' })
- .where(members: { user_id: ghost_user.id })
- .where(members: { requested_at: nil })
- .where(members: { access_level: described_class::ACCESS_LEVEL_OWNER })
- .where(
- described_class::Group
- .arel_table[:name]
- .matches("#{described_class::User::LOST_AND_FOUND_GROUP}%")
- )
- .count
- ).to eq(1)
- end
-
- it 'moves the orphaned project to the lost-and-found group' do
- orphaned_project = projects.find_by(name: 'project_with_non_existing_namespace')
- expect(orphaned_project.visibility_level).to eq(20)
- expect(orphaned_project.archived).to eq(false)
- expect(orphaned_project.namespace_id).to eq(non_existing_record_id)
-
- disable_migrations_output { migrate! }
-
- lost_and_found_group = described_class::Group.find_by(
- described_class::Group
- .arel_table[:name]
- .matches("#{described_class::User::LOST_AND_FOUND_GROUP}%")
- )
- orphaned_project = projects.find_by(id: orphaned_project.id)
-
- expect(orphaned_project.visibility_level).to eq(0)
- expect(orphaned_project.namespace_id).to eq(lost_and_found_group.id)
- expect(orphaned_project.name).to eq("project_with_non_existing_namespace_#{orphaned_project.id}")
- expect(orphaned_project.path).to eq("project_with_non_existing_namespace_#{orphaned_project.id}")
- expect(orphaned_project.archived).to eq(true)
-
- valid_project = projects.find_by(name: 'project_with_existing_namespace')
- existing_namespace = namespaces.find_by(name: 'existing_namespace')
-
- expect(valid_project.visibility_level).to eq(20)
- expect(valid_project.namespace_id).to eq(existing_namespace.id)
- expect(valid_project.path).to eq('project_with_existing_namespace')
- expect(valid_project.archived).to eq(false)
- end
-end
diff --git a/spec/migrations/cleanup_remaining_orphan_invites_spec.rb b/spec/migrations/cleanup_remaining_orphan_invites_spec.rb
index 0eb1f5a578a..987535a4f09 100644
--- a/spec/migrations/cleanup_remaining_orphan_invites_spec.rb
+++ b/spec/migrations/cleanup_remaining_orphan_invites_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration! 'cleanup_remaining_orphan_invites'
+require_migration!
RSpec.describe CleanupRemainingOrphanInvites, :migration do
def create_member(**extra_attributes)
diff --git a/spec/migrations/complete_namespace_settings_migration_spec.rb b/spec/migrations/complete_namespace_settings_migration_spec.rb
deleted file mode 100644
index 46c455d8b19..00000000000
--- a/spec/migrations/complete_namespace_settings_migration_spec.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe CompleteNamespaceSettingsMigration, :redis do
- let(:migration) { spy('migration') }
-
- context 'when still legacy artifacts exist' do
- let(:namespaces) { table(:namespaces) }
- let(:namespace_settings) { table(:namespace_settings) }
- let!(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') }
-
- it 'steals sidekiq jobs from BackfillNamespaceSettings background migration' do
- expect(Gitlab::BackgroundMigration).to receive(:steal).with('BackfillNamespaceSettings')
-
- migrate!
- end
-
- it 'migrates namespaces without namespace_settings' do
- expect { migrate! }.to change { namespace_settings.count }.from(0).to(1)
- end
- end
-end
diff --git a/spec/migrations/confirm_project_bot_users_spec.rb b/spec/migrations/confirm_project_bot_users_spec.rb
deleted file mode 100644
index 5f70181e70a..00000000000
--- a/spec/migrations/confirm_project_bot_users_spec.rb
+++ /dev/null
@@ -1,84 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe ConfirmProjectBotUsers, :migration do
- let(:users) { table(:users) }
-
- context 'project bot users that are currently unconfirmed' do
- let!(:project_bot_1) do
- create_user!(
- name: 'bot_1',
- email: 'bot_1@example.com',
- created_at: 2.days.ago,
- user_type: described_class::User::USER_TYPE_PROJECT_BOT
- )
- end
-
- let!(:project_bot_2) do
- create_user!(
- name: 'bot_2',
- email: 'bot_2@example.com',
- created_at: 4.days.ago,
- user_type: described_class::User::USER_TYPE_PROJECT_BOT
- )
- end
-
- it 'updates their `confirmed_at` attribute' do
- expect { migrate! }
- .to change { project_bot_1.reload.confirmed_at }
- .and change { project_bot_2.reload.confirmed_at }
- end
-
- it 'sets `confirmed_at` to be the same as their `created_at` attribute' do
- migrate!
-
- [project_bot_1, project_bot_2].each do |bot|
- expect(bot.reload.confirmed_at).to eq(bot.created_at)
- end
- end
- end
-
- context 'project bot users that are currently confirmed' do
- let!(:confirmed_project_bot) do
- create_user!(
- name: 'bot_1',
- email: 'bot_1@example.com',
- user_type: described_class::User::USER_TYPE_PROJECT_BOT,
- confirmed_at: 1.day.ago
- )
- end
-
- it 'does not update their `confirmed_at` attribute' do
- expect { migrate! }.not_to change { confirmed_project_bot.reload.confirmed_at }
- end
- end
-
- context 'human users that are currently unconfirmed' do
- let!(:unconfirmed_human) do
- create_user!(
- name: 'human',
- email: 'human@example.com',
- user_type: nil
- )
- end
-
- it 'does not update their `confirmed_at` attribute' do
- expect { migrate! }.not_to change { unconfirmed_human.reload.confirmed_at }
- end
- end
-
- private
-
- def create_user!(name:, email:, user_type:, created_at: Time.now, confirmed_at: nil)
- 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/create_environment_for_self_monitoring_project_spec.rb b/spec/migrations/create_environment_for_self_monitoring_project_spec.rb
deleted file mode 100644
index 4615c231510..00000000000
--- a/spec/migrations/create_environment_for_self_monitoring_project_spec.rb
+++ /dev/null
@@ -1,68 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe CreateEnvironmentForSelfMonitoringProject do
- let(:application_settings_table) { table(:application_settings) }
-
- let(:environments) { table(:environments) }
-
- let(:instance_administrators_group) do
- table(:namespaces).create!(
- id: 1,
- name: 'GitLab Instance Administrators',
- path: 'gitlab-instance-administrators-random',
- type: 'Group'
- )
- end
-
- let(:self_monitoring_project) do
- table(:projects).create!(
- id: 2,
- name: 'Self Monitoring',
- path: 'self_monitoring',
- namespace_id: instance_administrators_group.id
- )
- end
-
- context 'when the self monitoring project ID is not set' do
- it 'does not make changes' do
- expect(environments.find_by(project_id: self_monitoring_project.id)).to be_nil
-
- migrate!
-
- expect(environments.find_by(project_id: self_monitoring_project.id)).to be_nil
- end
- end
-
- context 'when the self monitoring project ID is set' do
- before do
- application_settings_table.create!(instance_administration_project_id: self_monitoring_project.id)
- end
-
- context 'when the environment already exists' do
- let!(:environment) do
- environments.create!(project_id: self_monitoring_project.id, name: 'production', slug: 'production')
- end
-
- it 'does not make changes' do
- expect(environments.find_by(project_id: self_monitoring_project.id)).to eq(environment)
-
- migrate!
-
- expect(environments.find_by(project_id: self_monitoring_project.id)).to eq(environment)
- end
- end
-
- context 'when the environment does not exist' do
- it 'creates the environment' do
- expect(environments.find_by(project_id: self_monitoring_project.id)).to be_nil
-
- migrate!
-
- expect(environments.find_by(project_id: self_monitoring_project.id)).to be
- end
- end
- end
-end
diff --git a/spec/migrations/deduplicate_epic_iids_spec.rb b/spec/migrations/deduplicate_epic_iids_spec.rb
deleted file mode 100644
index c9dd5b3253b..00000000000
--- a/spec/migrations/deduplicate_epic_iids_spec.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe DeduplicateEpicIids, :migration, schema: 20201106082723 do
- let(:routes) { table(:routes) }
- let(:epics) { table(:epics) }
- let(:users) { table(:users) }
- let(:namespaces) { table(:namespaces) }
-
- let!(:group) { create_group('foo') }
- let!(:user) { users.create!(email: 'test@example.com', projects_limit: 100, username: 'test') }
- let!(:dup_epic1) { epics.create!(iid: 1, title: 'epic 1', group_id: group.id, author_id: user.id, created_at: Time.now, updated_at: Time.now, title_html: 'any') }
- let!(:dup_epic2) { epics.create!(iid: 1, title: 'epic 2', group_id: group.id, author_id: user.id, created_at: Time.now, updated_at: Time.now, title_html: 'any') }
- let!(:dup_epic3) { epics.create!(iid: 1, title: 'epic 3', group_id: group.id, author_id: user.id, created_at: Time.now, updated_at: Time.now, title_html: 'any') }
-
- it 'deduplicates epic iids', :aggregate_failures do
- duplicate_epics_count = epics.where(iid: 1, group_id: group.id).count
- expect(duplicate_epics_count).to eq 3
-
- migrate!
-
- duplicate_epics_count = epics.where(iid: 1, group_id: group.id).count
- expect(duplicate_epics_count).to eq 1
- expect(dup_epic1.reload.iid).to eq 1
- expect(dup_epic2.reload.iid).to eq 2
- expect(dup_epic3.reload.iid).to eq 3
- end
-
- def create_group(path)
- namespaces.create!(name: path, path: path, type: 'Group').tap do |namespace|
- routes.create!(path: namespace.path, name: namespace.name, source_id: namespace.id, source_type: 'Namespace')
- end
- end
-end
diff --git a/spec/migrations/delete_internal_ids_where_feature_flags_usage_spec.rb b/spec/migrations/delete_internal_ids_where_feature_flags_usage_spec.rb
deleted file mode 100644
index 30d776c498b..00000000000
--- a/spec/migrations/delete_internal_ids_where_feature_flags_usage_spec.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe DeleteInternalIdsWhereFeatureFlagsUsage do
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:internal_ids) { table(:internal_ids) }
-
- def setup
- namespace = namespaces.create!(name: 'foo', path: 'foo')
- projects.create!(namespace_id: namespace.id)
- end
-
- it 'deletes feature flag rows from the internal_ids table' do
- project = setup
- internal_ids.create!(project_id: project.id, usage: 6, last_value: 1)
-
- disable_migrations_output { migrate! }
-
- expect(internal_ids.count).to eq(0)
- end
-
- it 'does not delete issue rows from the internal_ids table' do
- project = setup
- internal_ids.create!(project_id: project.id, usage: 0, last_value: 1)
-
- disable_migrations_output { migrate! }
-
- expect(internal_ids.count).to eq(1)
- end
-
- it 'does not delete merge request rows from the internal_ids table' do
- project = setup
- internal_ids.create!(project_id: project.id, usage: 1, last_value: 1)
-
- disable_migrations_output { migrate! }
-
- expect(internal_ids.count).to eq(1)
- end
-end
diff --git a/spec/migrations/delete_template_project_services_spec.rb b/spec/migrations/delete_template_project_services_spec.rb
deleted file mode 100644
index 20532e4187a..00000000000
--- a/spec/migrations/delete_template_project_services_spec.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe DeleteTemplateProjectServices, :migration do
- let(:services) { table(:services) }
- let(:project) { table(:projects).create!(namespace_id: 1) }
-
- before do
- services.create!(template: true, project_id: project.id)
- services.create!(template: true)
- services.create!(template: false, project_id: project.id)
- end
-
- it 'deletes services when template and attached to a project' do
- expect { migrate! }.to change { services.where(template: true, project_id: project.id).count }.from(1).to(0)
- .and not_change { services.where(template: true, project_id: nil).count }
- .and not_change { services.where(template: false).where.not(project_id: nil).count }
- end
-end
diff --git a/spec/migrations/delete_template_services_duplicated_by_type_spec.rb b/spec/migrations/delete_template_services_duplicated_by_type_spec.rb
deleted file mode 100644
index 577fea984da..00000000000
--- a/spec/migrations/delete_template_services_duplicated_by_type_spec.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe DeleteTemplateServicesDuplicatedByType do
- let(:services) { table(:services) }
-
- before do
- services.create!(template: true, type: 'JenkinsService')
- services.create!(template: true, type: 'JenkinsService')
- services.create!(template: true, type: 'JiraService')
- services.create!(template: true, type: 'JenkinsService')
- end
-
- it 'deletes service templates duplicated by type except the one with the lowest ID' do
- jenkins_integration_id = services.where(type: 'JenkinsService').order(:id).pluck(:id).first
- jira_integration_id = services.where(type: 'JiraService').pluck(:id).first
-
- migrate!
-
- expect(services.pluck(:id)).to contain_exactly(jenkins_integration_id, jira_integration_id)
- end
-end
diff --git a/spec/migrations/delete_user_callout_alerts_moved_spec.rb b/spec/migrations/delete_user_callout_alerts_moved_spec.rb
deleted file mode 100644
index 401cf77628d..00000000000
--- a/spec/migrations/delete_user_callout_alerts_moved_spec.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe DeleteUserCalloutAlertsMoved do
- let(:users) { table(:users) }
- let(:user_callouts) { table(:user_callouts) }
- let(:alerts_moved_feature) { described_class::FEATURE_NAME_ALERTS_MOVED }
- let(:unrelated_feature) { 1 }
-
- let!(:user1) { users.create!(email: '1', projects_limit: 0) }
- let!(:user2) { users.create!(email: '2', projects_limit: 0) }
-
- subject(:migration) { described_class.new }
-
- before do
- user_callouts.create!(user_id: user1.id, feature_name: alerts_moved_feature)
- user_callouts.create!(user_id: user1.id, feature_name: unrelated_feature)
- user_callouts.create!(user_id: user2.id, feature_name: alerts_moved_feature)
- end
-
- describe '#up' do
- it 'deletes `alerts_moved` user callouts' do
- migration.up
-
- expect(user_callouts.all.map(&:feature_name)).to eq([unrelated_feature])
- end
- end
-end
diff --git a/spec/migrations/drop_activate_prometheus_services_background_jobs_spec.rb b/spec/migrations/drop_activate_prometheus_services_background_jobs_spec.rb
deleted file mode 100644
index c6115d5889c..00000000000
--- a/spec/migrations/drop_activate_prometheus_services_background_jobs_spec.rb
+++ /dev/null
@@ -1,89 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe DropActivatePrometheusServicesBackgroundJobs, :sidekiq, :redis, schema: 2020_02_21_144534 do
- subject(:migration) { described_class.new }
-
- describe '#up' do
- let(:retry_set) { Sidekiq::RetrySet.new }
- let(:scheduled_set) { Sidekiq::ScheduledSet.new }
-
- context 'there are only affected jobs on the queue' do
- let(:payload) { { 'class' => ::BackgroundMigrationWorker, 'args' => [described_class::DROPPED_JOB_CLASS, 1] } }
- let(:queue_payload) { payload.merge('queue' => described_class::QUEUE) }
-
- it 'removes enqueued ActivatePrometheusServicesForSharedClusterApplications background jobs' do
- Sidekiq::Testing.disable! do # https://github.com/mperham/sidekiq/wiki/testing#api Sidekiq's API does not have a testing mode
- retry_set.schedule(1.hour.from_now, payload)
- scheduled_set.schedule(1.hour.from_now, payload)
- Sidekiq::Client.push(queue_payload)
-
- expect { migration.up }.to change { Sidekiq::Queue.new(described_class::QUEUE).size }.from(1).to(0)
- expect(retry_set.size).to eq(0)
- expect(scheduled_set.size).to eq(0)
- end
- end
- end
-
- context "there aren't any affected jobs on the queue" do
- let(:payload) { { 'class' => ::BackgroundMigrationWorker, 'args' => ['SomeOtherClass', 1] } }
- let(:queue_payload) { payload.merge('queue' => described_class::QUEUE) }
-
- it 'skips other enqueued jobs' do
- Sidekiq::Testing.disable! do
- retry_set.schedule(1.hour.from_now, payload)
- scheduled_set.schedule(1.hour.from_now, payload)
- Sidekiq::Client.push(queue_payload)
-
- expect { migration.up }.not_to change { Sidekiq::Queue.new(described_class::QUEUE).size }
- expect(retry_set.size).to eq(1)
- expect(scheduled_set.size).to eq(1)
- end
- end
- end
-
- context "there are multiple types of jobs on the queue" do
- let(:payload) { { 'class' => ::BackgroundMigrationWorker, 'args' => [described_class::DROPPED_JOB_CLASS, 1] } }
- let(:queue_payload) { payload.merge('queue' => described_class::QUEUE) }
-
- it 'skips other enqueued jobs' do
- Sidekiq::Testing.disable! do
- queue = Sidekiq::Queue.new(described_class::QUEUE)
- # these jobs will be deleted
- retry_set.schedule(1.hour.from_now, payload)
- scheduled_set.schedule(1.hour.from_now, payload)
- Sidekiq::Client.push(queue_payload)
- # this jobs will be skipped
- skipped_jobs_args = [['SomeOtherClass', 1], [described_class::DROPPED_JOB_CLASS, 'wrong id type'], [described_class::DROPPED_JOB_CLASS, 1, 'some wired argument']]
- skipped_jobs_args.each do |args|
- retry_set.schedule(1.hour.from_now, { 'class' => ::BackgroundMigrationWorker, 'args' => args })
- scheduled_set.schedule(1.hour.from_now, { 'class' => ::BackgroundMigrationWorker, 'args' => args })
- Sidekiq::Client.push('queue' => described_class::QUEUE, 'class' => ::BackgroundMigrationWorker, 'args' => args)
- end
-
- migration.up
-
- expect(retry_set.size).to be 3
- expect(scheduled_set.size).to be 3
- expect(queue.size).to be 3
- expect(queue.map(&:args)).to match_array skipped_jobs_args
- expect(retry_set.map(&:args)).to match_array skipped_jobs_args
- expect(scheduled_set.map(&:args)).to match_array skipped_jobs_args
- end
- end
- end
-
- context "other queues" do
- it 'does not modify them' do
- Sidekiq::Testing.disable! do
- Sidekiq::Client.push('queue' => 'other', 'class' => ::BackgroundMigrationWorker, 'args' => ['SomeOtherClass', 1])
- Sidekiq::Client.push('queue' => 'other', 'class' => ::BackgroundMigrationWorker, 'args' => [described_class::DROPPED_JOB_CLASS, 1])
-
- expect { migration.up }.not_to change { Sidekiq::Queue.new('other').size }
- end
- end
- end
- end
-end
diff --git a/spec/migrations/drop_background_migration_jobs_spec.rb b/spec/migrations/drop_background_migration_jobs_spec.rb
deleted file mode 100644
index 82b3f9f7187..00000000000
--- a/spec/migrations/drop_background_migration_jobs_spec.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe DropBackgroundMigrationJobs, :sidekiq, :redis, schema: 2020_01_16_051619 do
- subject(:migration) { described_class.new }
-
- describe '#up' do
- context 'there are only affected jobs on the queue' do
- it 'removes enqueued ActivatePrometheusServicesForSharedClusterApplications background jobs' do
- Sidekiq::Testing.disable! do # https://github.com/mperham/sidekiq/wiki/testing#api Sidekiq's API does not have a testing mode
- Sidekiq::Client.push('queue' => described_class::QUEUE, 'class' => ::BackgroundMigrationWorker, 'args' => [described_class::DROPPED_JOB_CLASS, 1])
-
- expect { migration.up }.to change { Sidekiq::Queue.new(described_class::QUEUE).size }.from(1).to(0)
- end
- end
- end
-
- context "there aren't any affected jobs on the queue" do
- it 'skips other enqueued jobs' do
- Sidekiq::Testing.disable! do
- Sidekiq::Client.push('queue' => described_class::QUEUE, 'class' => ::BackgroundMigrationWorker, 'args' => ['SomeOtherClass', 1])
-
- expect { migration.up }.not_to change { Sidekiq::Queue.new(described_class::QUEUE).size }
- end
- end
- end
-
- context "there are multiple types of jobs on the queue" do
- it 'skips other enqueued jobs' do
- Sidekiq::Testing.disable! do
- queue = Sidekiq::Queue.new(described_class::QUEUE)
- # this job will be deleted
- Sidekiq::Client.push('queue' => described_class::QUEUE, 'class' => ::BackgroundMigrationWorker, 'args' => [described_class::DROPPED_JOB_CLASS, 1])
- # this jobs will be skipped
- skipped_jobs_args = [['SomeOtherClass', 1], [described_class::DROPPED_JOB_CLASS, 'wrong id type'], [described_class::DROPPED_JOB_CLASS, 1, 'some wired argument']]
- skipped_jobs_args.each do |args|
- Sidekiq::Client.push('queue' => described_class::QUEUE, 'class' => ::BackgroundMigrationWorker, 'args' => args)
- end
-
- migration.up
-
- expect(queue.size).to be 3
- expect(queue.map(&:args)).to match_array skipped_jobs_args
- end
- end
- end
-
- context "other queues" do
- it 'does not modify them' do
- Sidekiq::Testing.disable! do
- Sidekiq::Client.push('queue' => 'other', 'class' => ::BackgroundMigrationWorker, 'args' => ['SomeOtherClass', 1])
- Sidekiq::Client.push('queue' => 'other', 'class' => ::BackgroundMigrationWorker, 'args' => [described_class::DROPPED_JOB_CLASS, 1])
-
- expect { migration.up }.not_to change { Sidekiq::Queue.new('other').size }
- end
- end
- end
- end
-end
diff --git a/spec/migrations/ensure_filled_external_diff_store_on_merge_request_diffs_spec.rb b/spec/migrations/ensure_filled_external_diff_store_on_merge_request_diffs_spec.rb
deleted file mode 100644
index 6998e7a91cf..00000000000
--- a/spec/migrations/ensure_filled_external_diff_store_on_merge_request_diffs_spec.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe EnsureFilledExternalDiffStoreOnMergeRequestDiffs, schema: 20200908095446 do
- let!(:merge_request_diffs) { table(:merge_request_diffs) }
- let!(:merge_requests) { table(:merge_requests) }
- let!(:namespaces) { table(:namespaces) }
- let!(:projects) { table(:projects) }
- let!(:namespace) { namespaces.create!(name: 'foo', path: 'foo') }
- let!(:project) { projects.create!(namespace_id: namespace.id) }
- let!(:merge_request) { merge_requests.create!(source_branch: 'x', target_branch: 'master', target_project_id: project.id) }
-
- before do
- constraint_name = 'check_93ee616ac9'
-
- # In order to insert a row with a NULL to fill.
- ActiveRecord::Base.connection.execute "ALTER TABLE merge_request_diffs DROP CONSTRAINT #{constraint_name}"
-
- @external_diff_store_1 = merge_request_diffs.create!(external_diff_store: 1, merge_request_id: merge_request.id)
- @external_diff_store_2 = merge_request_diffs.create!(external_diff_store: 2, merge_request_id: merge_request.id)
- @external_diff_store_nil = merge_request_diffs.create!(external_diff_store: nil, merge_request_id: merge_request.id)
-
- # revert DB structure
- ActiveRecord::Base.connection.execute "ALTER TABLE merge_request_diffs ADD CONSTRAINT #{constraint_name} CHECK ((external_diff_store IS NOT NULL)) NOT VALID"
- end
-
- it 'correctly migrates nil external_diff_store to 1' do
- migrate!
-
- @external_diff_store_1.reload
- @external_diff_store_2.reload
- @external_diff_store_nil.reload
-
- expect(@external_diff_store_1.external_diff_store).to eq(1) # unchanged
- expect(@external_diff_store_2.external_diff_store).to eq(2) # unchanged
- expect(@external_diff_store_nil.external_diff_store).to eq(1) # nil => 1
- end
-end
diff --git a/spec/migrations/ensure_filled_file_store_on_package_files_spec.rb b/spec/migrations/ensure_filled_file_store_on_package_files_spec.rb
deleted file mode 100644
index 5cfc3a6eeb8..00000000000
--- a/spec/migrations/ensure_filled_file_store_on_package_files_spec.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe EnsureFilledFileStoreOnPackageFiles, schema: 20200910175553 do
- let!(:packages_package_files) { table(:packages_package_files) }
- let!(:packages_packages) { table(:packages_packages) }
- let!(:namespaces) { table(:namespaces) }
- let!(:projects) { table(:projects) }
- let!(:namespace) { namespaces.create!(name: 'foo', path: 'foo') }
- let!(:project) { projects.create!(namespace_id: namespace.id) }
- let!(:package) { packages_packages.create!(project_id: project.id, name: 'bar', package_type: 1) }
-
- before do
- constraint_name = 'check_4c5e6bb0b3'
-
- # In order to insert a row with a NULL to fill.
- ActiveRecord::Base.connection.execute "ALTER TABLE packages_package_files DROP CONSTRAINT #{constraint_name}"
-
- @file_store_1 = packages_package_files.create!(file_store: 1, file_name: 'foo_1', file: 'foo_1', package_id: package.id)
- @file_store_2 = packages_package_files.create!(file_store: 2, file_name: 'foo_2', file: 'foo_2', package_id: package.id)
- @file_store_nil = packages_package_files.create!(file_store: nil, file_name: 'foo_nil', file: 'foo_nil', package_id: package.id)
-
- # revert DB structure
- ActiveRecord::Base.connection.execute "ALTER TABLE packages_package_files ADD CONSTRAINT #{constraint_name} CHECK ((file_store IS NOT NULL)) NOT VALID"
- end
-
- it 'correctly migrates nil file_store to 1' do
- migrate!
-
- @file_store_1.reload
- @file_store_2.reload
- @file_store_nil.reload
-
- expect(@file_store_1.file_store).to eq(1) # unchanged
- expect(@file_store_2.file_store).to eq(2) # unchanged
- expect(@file_store_nil.file_store).to eq(1) # nil => 1
- end
-end
diff --git a/spec/migrations/ensure_namespace_settings_creation_spec.rb b/spec/migrations/ensure_namespace_settings_creation_spec.rb
deleted file mode 100644
index b105e678d35..00000000000
--- a/spec/migrations/ensure_namespace_settings_creation_spec.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe EnsureNamespaceSettingsCreation do
- context 'when there are namespaces without namespace settings' do
- let(:namespaces) { table(:namespaces) }
- let(:namespace_settings) { table(:namespace_settings) }
- let!(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') }
- let!(:namespace_2) { namespaces.create!(name: 'gitlab', path: 'gitlab-org2') }
-
- it 'migrates namespaces without namespace_settings' 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.to_i, namespace.id, namespace_2.id)
- end
- end
- end
-
- it 'schedules migrations in batches' do
- stub_const("#{described_class.name}::BATCH_SIZE", 2)
-
- namespace_3 = namespaces.create!(name: 'gitlab', path: 'gitlab-org3')
- namespace_4 = namespaces.create!(name: 'gitlab', path: 'gitlab-org4')
-
- Sidekiq::Testing.fake! do
- freeze_time do
- migrate!
-
- expect(described_class::MIGRATION)
- .to be_scheduled_delayed_migration(2.minutes.to_i, namespace.id, namespace_2.id)
- expect(described_class::MIGRATION)
- .to be_scheduled_delayed_migration(4.minutes.to_i, namespace_3.id, namespace_4.id)
- end
- end
- end
- end
-end
diff --git a/spec/migrations/ensure_target_project_id_is_filled_spec.rb b/spec/migrations/ensure_target_project_id_is_filled_spec.rb
deleted file mode 100644
index 7a9f49390fb..00000000000
--- a/spec/migrations/ensure_target_project_id_is_filled_spec.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe EnsureTargetProjectIdIsFilled, schema: 20200827085101 do
- let_it_be(:namespaces) { table(:namespaces) }
- let_it_be(:projects) { table(:projects) }
- let_it_be(:merge_requests) { table(:merge_requests) }
- let_it_be(:metrics) { table(:merge_request_metrics) }
-
- let!(:namespace) { namespaces.create!(name: 'namespace', path: 'namespace') }
- let!(:project_1) { projects.create!(namespace_id: namespace.id) }
- let!(:project_2) { projects.create!(namespace_id: namespace.id) }
- let!(:merge_request_to_migrate_1) { merge_requests.create!(source_branch: 'a', target_branch: 'b', target_project_id: project_1.id) }
- let!(:merge_request_to_migrate_2) { merge_requests.create!(source_branch: 'c', target_branch: 'd', target_project_id: project_2.id) }
- let!(:merge_request_not_to_migrate) { merge_requests.create!(source_branch: 'e', target_branch: 'f', target_project_id: project_1.id) }
-
- let!(:metrics_1) { metrics.create!(merge_request_id: merge_request_to_migrate_1.id) }
- let!(:metrics_2) { metrics.create!(merge_request_id: merge_request_to_migrate_2.id) }
- let!(:metrics_3) { metrics.create!(merge_request_id: merge_request_not_to_migrate.id, target_project_id: project_1.id) }
-
- it 'migrates missing target_project_ids' do
- migrate!
-
- expect(metrics_1.reload.target_project_id).to eq(project_1.id)
- expect(metrics_2.reload.target_project_id).to eq(project_2.id)
- expect(metrics_3.reload.target_project_id).to eq(project_1.id)
- end
-end
diff --git a/spec/migrations/ensure_u2f_registrations_migrated_spec.rb b/spec/migrations/ensure_u2f_registrations_migrated_spec.rb
deleted file mode 100644
index 01db29c0edf..00000000000
--- a/spec/migrations/ensure_u2f_registrations_migrated_spec.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe EnsureU2fRegistrationsMigrated, schema: 20201022144501 do
- let(:u2f_registrations) { table(:u2f_registrations) }
- let(:webauthn_registrations) { table(:webauthn_registrations) }
- let(:users) { table(:users) }
-
- let(:user) { users.create!(email: 'email@email.com', name: 'foo', username: 'foo', projects_limit: 0) }
-
- before do
- create_u2f_registration(1, 'reg1')
- create_u2f_registration(2, 'reg2')
- webauthn_registrations.create!({ name: 'reg1', u2f_registration_id: 1, credential_xid: '', public_key: '', user_id: user.id })
- end
-
- it 'correctly migrates u2f registrations previously not migrated' do
- expect { migrate! }.to change { webauthn_registrations.count }.from(1).to(2)
- end
-
- it 'migrates all valid u2f registrations depite errors' do
- create_u2f_registration(3, 'reg3', 'invalid!')
- create_u2f_registration(4, 'reg4')
-
- expect { migrate! }.to change { webauthn_registrations.count }.from(1).to(3)
- end
-
- def create_u2f_registration(id, name, public_key = nil)
- device = U2F::FakeU2F.new(FFaker::BaconIpsum.characters(5), { key_handle: SecureRandom.random_bytes(255) })
- public_key ||= Base64.strict_encode64(device.origin_public_key_raw)
- u2f_registrations.create!({ id: id,
- certificate: Base64.strict_encode64(device.cert_raw),
- key_handle: U2F.urlsafe_encode64(device.key_handle_raw),
- public_key: public_key,
- counter: 5,
- name: name,
- user_id: user.id })
- end
-end
diff --git a/spec/migrations/fill_file_store_ci_job_artifacts_spec.rb b/spec/migrations/fill_file_store_ci_job_artifacts_spec.rb
deleted file mode 100644
index 7adcf74bdba..00000000000
--- a/spec/migrations/fill_file_store_ci_job_artifacts_spec.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe FillFileStoreCiJobArtifacts do
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:builds) { table(:ci_builds) }
- let(:job_artifacts) { table(:ci_job_artifacts) }
-
- before do
- namespaces.create!(id: 123, name: 'sample', path: 'sample')
- projects.create!(id: 123, name: 'sample', path: 'sample', namespace_id: 123)
- builds.create!(id: 1)
- end
-
- context 'when file_store is nil' do
- it 'updates file_store to local' do
- job_artifacts.create!(project_id: 123, job_id: 1, file_type: 1, file_store: nil)
- job_artifact = job_artifacts.find_by(project_id: 123, job_id: 1)
-
- expect { migrate! }.to change { job_artifact.reload.file_store }.from(nil).to(1)
- end
- end
-
- context 'when file_store is set to local' do
- it 'does not update file_store' do
- job_artifacts.create!(project_id: 123, job_id: 1, file_type: 1, file_store: 1)
- job_artifact = job_artifacts.find_by(project_id: 123, job_id: 1)
-
- expect { migrate! }.not_to change { job_artifact.reload.file_store }
- end
- end
-
- context 'when file_store is set to object storage' do
- it 'does not update file_store' do
- job_artifacts.create!(project_id: 123, job_id: 1, file_type: 1, file_store: 2)
- job_artifact = job_artifacts.find_by(project_id: 123, job_id: 1)
-
- expect { migrate! }.not_to change { job_artifact.reload.file_store }
- end
- end
-end
diff --git a/spec/migrations/fill_file_store_lfs_objects_spec.rb b/spec/migrations/fill_file_store_lfs_objects_spec.rb
deleted file mode 100644
index 688976f79e8..00000000000
--- a/spec/migrations/fill_file_store_lfs_objects_spec.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe FillFileStoreLfsObjects do
- let(:lfs_objects) { table(:lfs_objects) }
- let(:oid) { 'b804383982bb89b00e828e3f44c038cc991d3d1768009fc39ba8e2c081b9fb75' }
-
- context 'when file_store is nil' do
- it 'updates file_store to local' do
- lfs_objects.create!(oid: oid, size: 1062, file_store: nil)
- lfs_object = lfs_objects.find_by(oid: oid)
-
- expect { migrate! }.to change { lfs_object.reload.file_store }.from(nil).to(1)
- end
- end
-
- context 'when file_store is set to local' do
- it 'does not update file_store' do
- lfs_objects.create!(oid: oid, size: 1062, file_store: 1)
- lfs_object = lfs_objects.find_by(oid: oid)
-
- expect { migrate! }.not_to change { lfs_object.reload.file_store }
- end
- end
-
- context 'when file_store is set to object storage' do
- it 'does not update file_store' do
- lfs_objects.create!(oid: oid, size: 1062, file_store: 2)
- lfs_object = lfs_objects.find_by(oid: oid)
-
- expect { migrate! }.not_to change { lfs_object.reload.file_store }
- end
- end
-end
diff --git a/spec/migrations/fill_store_uploads_spec.rb b/spec/migrations/fill_store_uploads_spec.rb
deleted file mode 100644
index 19db7c2b48d..00000000000
--- a/spec/migrations/fill_store_uploads_spec.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe FillStoreUploads do
- let(:uploads) { table(:uploads) }
- let(:path) { 'uploads/-/system/avatar.jpg' }
-
- context 'when store is nil' do
- it 'updates store to local' do
- uploads.create!(size: 100.kilobytes,
- uploader: 'AvatarUploader',
- path: path,
- store: nil)
-
- upload = uploads.find_by(path: path)
-
- expect { migrate! }.to change { upload.reload.store }.from(nil).to(1)
- end
- end
-
- context 'when store is set to local' do
- it 'does not update store' do
- uploads.create!(size: 100.kilobytes,
- uploader: 'AvatarUploader',
- path: path,
- store: 1)
-
- upload = uploads.find_by(path: path)
-
- expect { migrate! }.not_to change { upload.reload.store }
- end
- end
-
- context 'when store is set to object storage' do
- it 'does not update store' do
- uploads.create!(size: 100.kilobytes,
- uploader: 'AvatarUploader',
- path: path,
- store: 2)
-
- upload = uploads.find_by(path: path)
-
- expect { migrate! }.not_to change { upload.reload.store }
- end
- end
-end
diff --git a/spec/migrations/fix_projects_without_project_feature_spec.rb b/spec/migrations/fix_projects_without_project_feature_spec.rb
deleted file mode 100644
index d8c5e7a28c0..00000000000
--- a/spec/migrations/fix_projects_without_project_feature_spec.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe FixProjectsWithoutProjectFeature do
- let(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab-org') }
-
- let!(:projects) do
- [
- table(:projects).create!(namespace_id: namespace.id, name: 'foo 1'),
- table(:projects).create!(namespace_id: namespace.id, name: 'foo 2'),
- table(:projects).create!(namespace_id: namespace.id, name: 'foo 3')
- ]
- end
-
- before do
- stub_const("#{described_class.name}::BATCH_SIZE", 2)
- end
-
- around do |example|
- Sidekiq::Testing.fake! do
- freeze_time do
- example.call
- end
- end
- end
-
- it 'schedules jobs for ranges of projects' do
- migrate!
-
- expect(described_class::MIGRATION)
- .to be_scheduled_delayed_migration(2.minutes, projects[0].id, projects[1].id)
-
- expect(described_class::MIGRATION)
- .to be_scheduled_delayed_migration(4.minutes, projects[2].id, projects[2].id)
- end
-
- it 'schedules jobs according to the configured batch size' do
- expect { migrate! }.to change { BackgroundMigrationWorker.jobs.size }.by(2)
- end
-end
diff --git a/spec/migrations/fix_projects_without_prometheus_services_spec.rb b/spec/migrations/fix_projects_without_prometheus_services_spec.rb
deleted file mode 100644
index dc03f381abd..00000000000
--- a/spec/migrations/fix_projects_without_prometheus_services_spec.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-#
-require 'spec_helper'
-require_migration!('fix_projects_without_prometheus_service')
-
-RSpec.describe FixProjectsWithoutPrometheusService, :migration do
- let(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab-org') }
-
- let!(:projects) do
- [
- table(:projects).create!(namespace_id: namespace.id, name: 'foo 1'),
- table(:projects).create!(namespace_id: namespace.id, name: 'foo 2'),
- table(:projects).create!(namespace_id: namespace.id, name: 'foo 3')
- ]
- end
-
- before do
- stub_const("#{described_class.name}::BATCH_SIZE", 2)
- end
-
- around do |example|
- Sidekiq::Testing.fake! do
- freeze_time do
- example.call
- end
- end
- end
-
- it 'schedules jobs for ranges of projects' do
- migrate!
-
- expect(described_class::MIGRATION)
- .to be_scheduled_delayed_migration(2.minutes, projects[0].id, projects[1].id)
-
- expect(described_class::MIGRATION)
- .to be_scheduled_delayed_migration(4.minutes, projects[2].id, projects[2].id)
- end
-
- it 'schedules jobs according to the configured batch size' do
- expect { migrate! }.to change { BackgroundMigrationWorker.jobs.size }.by(2)
- end
-end
diff --git a/spec/migrations/generate_ci_jwt_signing_key_spec.rb b/spec/migrations/generate_ci_jwt_signing_key_spec.rb
deleted file mode 100644
index 7a895284aa1..00000000000
--- a/spec/migrations/generate_ci_jwt_signing_key_spec.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-RSpec.describe GenerateCiJwtSigningKey do
- let(:application_settings) do
- Class.new(ActiveRecord::Base) do
- self.table_name = 'application_settings'
-
- attr_encrypted :ci_jwt_signing_key, {
- mode: :per_attribute_iv,
- key: Gitlab::Utils.ensure_utf8_size(Rails.application.secrets.db_key_base, bytes: 32.bytes),
- algorithm: 'aes-256-gcm',
- encode: true
- }
- end
- end
-
- it 'generates JWT signing key' do
- application_settings.create!
-
- reversible_migration do |migration|
- migration.before -> {
- settings = application_settings.first
-
- expect(settings.ci_jwt_signing_key).to be_nil
- expect(settings.encrypted_ci_jwt_signing_key).to be_nil
- expect(settings.encrypted_ci_jwt_signing_key_iv).to be_nil
- }
-
- migration.after -> {
- settings = application_settings.first
-
- expect(settings.encrypted_ci_jwt_signing_key).to be_present
- expect(settings.encrypted_ci_jwt_signing_key_iv).to be_present
- expect { OpenSSL::PKey::RSA.new(settings.ci_jwt_signing_key) }.not_to raise_error
- }
- end
- end
-end
diff --git a/spec/migrations/generate_missing_routes_for_bots_spec.rb b/spec/migrations/generate_missing_routes_for_bots_spec.rb
deleted file mode 100644
index 594e51b4410..00000000000
--- a/spec/migrations/generate_missing_routes_for_bots_spec.rb
+++ /dev/null
@@ -1,80 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-RSpec.describe GenerateMissingRoutesForBots, :migration do
- let(:users) { table(:users) }
- let(:namespaces) { table(:namespaces) }
- let(:routes) { table(:routes) }
-
- let(:visual_review_bot) do
- users.create!(email: 'visual-review-bot@gitlab.com', name: 'GitLab Visual Review Bot', username: 'visual-review-bot', user_type: 3, projects_limit: 5)
- end
-
- let(:migration_bot) do
- users.create!(email: 'migration-bot@gitlab.com', name: 'GitLab Migration Bot', username: 'migration-bot', user_type: 7, projects_limit: 5)
- end
-
- let!(:visual_review_bot_namespace) do
- namespaces.create!(owner_id: visual_review_bot.id, name: visual_review_bot.name, path: visual_review_bot.username)
- end
-
- let!(:migration_bot_namespace) do
- namespaces.create!(owner_id: migration_bot.id, name: migration_bot.name, path: migration_bot.username)
- end
-
- context 'for bot users without an existing route' do
- it 'creates new routes' do
- expect { migrate! }.to change { routes.count }.by(2)
- end
-
- it 'creates new routes with the same path and name as their namespace' do
- migrate!
-
- [visual_review_bot, migration_bot].each do |bot|
- namespace = namespaces.find_by(owner_id: bot.id)
- route = route_for(namespace: namespace)
-
- expect(route.path).to eq(namespace.path)
- expect(route.name).to eq(namespace.name)
- end
- end
- end
-
- it 'does not create routes for bot users with existing routes' do
- create_route!(namespace: visual_review_bot_namespace)
- create_route!(namespace: migration_bot_namespace)
-
- expect { migrate! }.not_to change { routes.count }
- end
-
- it 'does not create routes for human users without an existing route' do
- human_namespace = create_human_namespace!(name: 'GitLab Human', username: 'human')
-
- expect { migrate! }.not_to change { route_for(namespace: human_namespace) }
- end
-
- it 'does not create route for a bot user with a missing route, if a human user with the same path already exists' do
- human_namespace = create_human_namespace!(name: visual_review_bot.name, username: visual_review_bot.username)
- create_route!(namespace: human_namespace)
-
- expect { migrate! }.not_to change { route_for(namespace: visual_review_bot_namespace) }
- end
-
- private
-
- def create_human_namespace!(name:, username:)
- human = users.create!(email: 'human@gitlab.com', name: name, username: username, user_type: nil, projects_limit: 5)
- namespaces.create!(owner_id: human.id, name: human.name, path: human.username)
- end
-
- def create_route!(namespace:)
- routes.create!(path: namespace.path, name: namespace.name, source_id: namespace.id, source_type: 'Namespace')
- end
-
- def route_for(namespace:)
- routes.find_by(source_type: 'Namespace', source_id: namespace.id)
- end
-end
diff --git a/spec/migrations/insert_daily_invites_plan_limits_spec.rb b/spec/migrations/insert_daily_invites_plan_limits_spec.rb
deleted file mode 100644
index 49d41a1039f..00000000000
--- a/spec/migrations/insert_daily_invites_plan_limits_spec.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe InsertDailyInvitesPlanLimits do
- let(:plans) { table(:plans) }
- let(:plan_limits) { table(:plan_limits) }
- let!(:free_plan) { plans.create!(name: 'free') }
- let!(:bronze_plan) { plans.create!(name: 'bronze') }
- let!(:silver_plan) { plans.create!(name: 'silver') }
- let!(:gold_plan) { plans.create!(name: 'gold') }
-
- context 'when on Gitlab.com' do
- before do
- expect(Gitlab).to receive(:com?).at_most(:twice).and_return(true)
- end
-
- it 'correctly migrates up and down' do
- reversible_migration do |migration|
- migration.before -> {
- expect(plan_limits.where.not(daily_invites: 0)).to be_empty
- }
-
- # Expectations will run after the up migration.
- migration.after -> {
- expect(plan_limits.pluck(:plan_id, :daily_invites)).to contain_exactly(
- [free_plan.id, 20],
- [bronze_plan.id, 0],
- [silver_plan.id, 0],
- [gold_plan.id, 0]
- )
- }
- end
- end
- end
-
- context 'when on self hosted' do
- before do
- expect(Gitlab).to receive(:com?).at_most(:twice).and_return(false)
- end
-
- it 'correctly migrates up and down' do
- reversible_migration do |migration|
- migration.before -> {
- expect(plan_limits.pluck(:daily_invites)).to eq []
- }
-
- migration.after -> {
- expect(plan_limits.pluck(:daily_invites)).to eq []
- }
- end
- end
- end
-end
diff --git a/spec/migrations/insert_project_feature_flags_plan_limits_spec.rb b/spec/migrations/insert_project_feature_flags_plan_limits_spec.rb
deleted file mode 100644
index 481e987c188..00000000000
--- a/spec/migrations/insert_project_feature_flags_plan_limits_spec.rb
+++ /dev/null
@@ -1,76 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe InsertProjectFeatureFlagsPlanLimits do
- let(:migration) { described_class.new }
- let(:plans) { table(:plans) }
- let(:plan_limits) { table(:plan_limits) }
- let!(:default_plan) { plans.create!(name: 'default') }
- let!(:free_plan) { plans.create!(name: 'free') }
- let!(:bronze_plan) { plans.create!(name: 'bronze') }
- let!(:silver_plan) { plans.create!(name: 'silver') }
- let!(:gold_plan) { plans.create!(name: 'gold') }
- let!(:default_plan_limits) do
- plan_limits.create!(plan_id: default_plan.id, project_feature_flags: 200)
- end
-
- context 'when on Gitlab.com' do
- before do
- expect(Gitlab).to receive(:com?).at_most(:twice).and_return(true)
- end
-
- describe '#up' do
- it 'updates the project_feature_flags plan limits' do
- migration.up
-
- expect(plan_limits.pluck(:plan_id, :project_feature_flags)).to contain_exactly(
- [default_plan.id, 200],
- [free_plan.id, 50],
- [bronze_plan.id, 100],
- [silver_plan.id, 150],
- [gold_plan.id, 200]
- )
- end
- end
-
- describe '#down' do
- it 'removes the project_feature_flags plan limits' do
- migration.up
- migration.down
-
- expect(plan_limits.pluck(:plan_id, :project_feature_flags)).to contain_exactly(
- [default_plan.id, 200],
- [free_plan.id, 0],
- [bronze_plan.id, 0],
- [silver_plan.id, 0],
- [gold_plan.id, 0]
- )
- end
- end
- end
-
- context 'when on self-hosted' do
- before do
- expect(Gitlab).to receive(:com?).at_most(:twice).and_return(false)
- end
-
- describe '#up' do
- it 'does not change the plan limits' do
- migration.up
-
- expect(plan_limits.pluck(:project_feature_flags)).to contain_exactly(200)
- end
- end
-
- describe '#down' do
- it 'does not change the plan limits' do
- migration.up
- migration.down
-
- expect(plan_limits.pluck(:project_feature_flags)).to contain_exactly(200)
- end
- end
- end
-end
diff --git a/spec/migrations/migrate_all_merge_request_user_mentions_to_db_spec.rb b/spec/migrations/migrate_all_merge_request_user_mentions_to_db_spec.rb
deleted file mode 100644
index c2df04bf2d6..00000000000
--- a/spec/migrations/migrate_all_merge_request_user_mentions_to_db_spec.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe MigrateAllMergeRequestUserMentionsToDb, :migration do
- let(:users) { table(:users) }
- let(:projects) { table(:projects) }
- let(:namespaces) { table(:namespaces) }
- let(:merge_requests) { table(:merge_requests) }
- let(:merge_request_user_mentions) { table(:merge_request_user_mentions) }
-
- let(:user) { users.create!(name: 'root', email: 'root@example.com', username: 'root', projects_limit: 0) }
- let(:group) { namespaces.create!(name: 'group1', path: 'group1', owner_id: user.id, type: 'Group') }
- let(:project) { projects.create!(name: 'gitlab1', path: 'gitlab1', namespace_id: group.id, visibility_level: 0) }
-
- let(:opened_state) { 1 }
- let(:closed_state) { 2 }
- let(:merged_state) { 3 }
-
- # migrateable resources
- let(:common_args) { { source_branch: 'master', source_project_id: project.id, target_project_id: project.id, author_id: user.id, description: 'mr description with @root mention' } }
- let!(:resource1) { merge_requests.create!(common_args.merge(title: "title 1", state_id: opened_state, target_branch: 'feature1')) }
- let!(:resource2) { merge_requests.create!(common_args.merge(title: "title 2", state_id: closed_state, target_branch: 'feature2')) }
- let!(:resource3) { merge_requests.create!(common_args.merge(title: "title 3", state_id: merged_state, target_branch: 'feature3')) }
-
- # non-migrateable resources
- # this merge request is already migrated, as it has a record in the merge_request_user_mentions table
- let!(:resource4) { merge_requests.create!(common_args.merge(title: "title 3", state_id: opened_state, target_branch: 'feature4')) }
- let!(:user_mention) { merge_request_user_mentions.create!(merge_request_id: resource4.id, mentioned_users_ids: [1]) }
-
- let!(:resource5) { merge_requests.create!(common_args.merge(title: "title 3", description: 'description with no mention', state_id: opened_state, target_branch: 'feature5')) }
-
- it_behaves_like 'schedules resource mentions migration', MergeRequest, false
-end
diff --git a/spec/migrations/migrate_bot_type_to_user_type_spec.rb b/spec/migrations/migrate_bot_type_to_user_type_spec.rb
deleted file mode 100644
index 54cf3450692..00000000000
--- a/spec/migrations/migrate_bot_type_to_user_type_spec.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-RSpec.describe MigrateBotTypeToUserType, :migration do
- let(:users) { table(:users) }
-
- it 'updates bots & ignores humans' do
- users.create!(email: 'human', bot_type: nil, projects_limit: 0)
- users.create!(email: 'support_bot', bot_type: 1, projects_limit: 0)
- users.create!(email: 'alert_bot', bot_type: 2, projects_limit: 0)
- users.create!(email: 'visual_review_bot', bot_type: 3, projects_limit: 0)
-
- migrate!
-
- expect(users.where.not(user_type: nil).map(&:user_type)).to match_array([1, 2, 3])
- end
-end
diff --git a/spec/migrations/migrate_commit_notes_mentions_to_db_spec.rb b/spec/migrations/migrate_commit_notes_mentions_to_db_spec.rb
deleted file mode 100644
index aa2aa6297c4..00000000000
--- a/spec/migrations/migrate_commit_notes_mentions_to_db_spec.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe MigrateCommitNotesMentionsToDb, :migration, :sidekiq do
- let(:users) { table(:users) }
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:notes) { table(:notes) }
-
- let(:user) { users.create!(name: 'root', email: 'root@example.com', username: 'root', projects_limit: 0) }
- let(:group) { namespaces.create!(name: 'group1', path: 'group1', owner_id: user.id) }
- let(:project) { projects.create!(name: 'gitlab1', path: 'gitlab1', namespace_id: group.id, visibility_level: 0) }
-
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') }
- let(:commit) { Commit.new(RepoHelpers.sample_commit, project) }
- let(:commit_user_mentions) { table(:commit_user_mentions) }
-
- let!(:resource1) { notes.create!(commit_id: commit.id, noteable_type: 'Commit', project_id: project.id, author_id: user.id, note: 'note1 for @root to check') }
- let!(:resource2) { notes.create!(commit_id: commit.id, noteable_type: 'Commit', project_id: project.id, author_id: user.id, note: 'note1 for @root to check') }
- let!(:resource3) { notes.create!(commit_id: commit.id, noteable_type: 'Commit', project_id: project.id, author_id: user.id, note: 'note1 for @root to check', system: true) }
-
- # non-migrateable resources
- # this note is already migrated, as it has a record in the commit_user_mentions table
- let!(:resource4) { notes.create!(note: 'note3 for @root to check', commit_id: commit.id, noteable_type: 'Commit') }
- let!(:user_mention) { commit_user_mentions.create!(commit_id: commit.id, note_id: resource4.id, mentioned_users_ids: [1]) }
- # this should have pointed to an inexistent commit record in a commits table
- # but because commit is not an AR, we'll just make it so that the note does not have mentions, i.e. no `@` char.
- let!(:resource5) { notes.create!(note: 'note3 to check', commit_id: 'abc', noteable_type: 'Commit') }
-
- before do
- stub_const("#{described_class.name}::BATCH_SIZE", 1)
- end
-
- it_behaves_like 'schedules resource mentions migration', Commit, true
-end
diff --git a/spec/migrations/migrate_compliance_framework_enum_to_database_framework_record_spec.rb b/spec/migrations/migrate_compliance_framework_enum_to_database_framework_record_spec.rb
deleted file mode 100644
index 6a9a75a7019..00000000000
--- a/spec/migrations/migrate_compliance_framework_enum_to_database_framework_record_spec.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe MigrateComplianceFrameworkEnumToDatabaseFrameworkRecord, schema: 20201005092753 do
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:project_compliance_framework_settings) { table(:project_compliance_framework_settings) }
- let(:compliance_management_frameworks) { table(:compliance_management_frameworks) }
-
- let(:gdpr_framework) { 1 }
- let(:sox_framework) { 5 }
-
- let!(:root_group) { namespaces.create!(type: 'Group', name: 'a', path: 'a') }
- let!(:sub_group) { namespaces.create!(type: 'Group', name: 'b', path: 'b', parent_id: root_group.id) }
- let!(:sub_sub_group) { namespaces.create!(type: 'Group', name: 'c', path: 'c', parent_id: sub_group.id) }
-
- let!(:namespace) { namespaces.create!(name: 'd', path: 'd') }
-
- let!(:project_on_root_level) { projects.create!(namespace_id: root_group.id) }
- let!(:project_on_sub_sub_level_1) { projects.create!(namespace_id: sub_sub_group.id) }
- let!(:project_on_sub_sub_level_2) { projects.create!(namespace_id: sub_sub_group.id) }
- let!(:project_on_namespace) { projects.create!(namespace_id: namespace.id) }
-
- let!(:project_on_root_level_compliance_setting) { project_compliance_framework_settings.create!(project_id: project_on_root_level.id, framework: gdpr_framework) }
- let!(:project_on_sub_sub_level_compliance_setting_1) { project_compliance_framework_settings.create!(project_id: project_on_sub_sub_level_1.id, framework: sox_framework) }
- let!(:project_on_sub_sub_level_compliance_setting_2) { project_compliance_framework_settings.create!(project_id: project_on_sub_sub_level_2.id, framework: gdpr_framework) }
- let!(:project_on_namespace_level_compliance_setting) { project_compliance_framework_settings.create!(project_id: project_on_namespace.id, framework: gdpr_framework) }
-
- subject { described_class.new.up }
-
- it 'updates the project settings' do
- subject
-
- gdpr_framework = compliance_management_frameworks.find_by(namespace_id: root_group.id, name: 'GDPR')
- expect(project_on_root_level_compliance_setting.reload.framework_id).to eq(gdpr_framework.id)
- expect(project_on_sub_sub_level_compliance_setting_2.reload.framework_id).to eq(gdpr_framework.id)
-
- sox_framework = compliance_management_frameworks.find_by(namespace_id: root_group.id, name: 'SOX')
- expect(project_on_sub_sub_level_compliance_setting_1.reload.framework_id).to eq(sox_framework.id)
-
- gdpr_framework = compliance_management_frameworks.find_by(namespace_id: namespace.id, name: 'GDPR')
- expect(project_on_namespace_level_compliance_setting.reload.framework_id).to eq(gdpr_framework.id)
- end
-
- it 'adds two framework records' do
- subject
-
- expect(compliance_management_frameworks.count).to eq(3)
- end
-end
diff --git a/spec/migrations/migrate_create_commit_signature_worker_sidekiq_queue_spec.rb b/spec/migrations/migrate_create_commit_signature_worker_sidekiq_queue_spec.rb
deleted file mode 100644
index 0e631f255bf..00000000000
--- a/spec/migrations/migrate_create_commit_signature_worker_sidekiq_queue_spec.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe MigrateCreateCommitSignatureWorkerSidekiqQueue, :sidekiq, :redis do
- include Gitlab::Database::MigrationHelpers
- include StubWorker
-
- context 'when there are jobs in the queue' do
- it 'correctly migrates queue when migrating up' do
- Sidekiq::Testing.disable! do
- stub_worker(queue: 'create_commit_signature').perform_async('Something', [1])
- stub_worker(queue: 'create_gpg_signature').perform_async('Something', [1])
-
- described_class.new.up
-
- expect(sidekiq_queue_length('create_gpg_signature')).to eq 0
- expect(sidekiq_queue_length('create_commit_signature')).to eq 2
- end
- end
-
- it 'correctly migrates queue when migrating down' do
- Sidekiq::Testing.disable! do
- stub_worker(queue: 'create_gpg_signature').perform_async('Something', [1])
-
- described_class.new.down
-
- expect(sidekiq_queue_length('create_gpg_signature')).to eq 1
- expect(sidekiq_queue_length('create_commit_signature')).to eq 0
- end
- end
- end
-
- context 'when there are no jobs in the queues' do
- it 'does not raise error when migrating up' do
- expect { described_class.new.up }.not_to raise_error
- end
-
- it 'does not raise error when migrating down' do
- expect { described_class.new.down }.not_to raise_error
- end
- end
-end
diff --git a/spec/migrations/migrate_incident_issues_to_incident_type_spec.rb b/spec/migrations/migrate_incident_issues_to_incident_type_spec.rb
deleted file mode 100644
index acac6114c71..00000000000
--- a/spec/migrations/migrate_incident_issues_to_incident_type_spec.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe MigrateIncidentIssuesToIncidentType do
- let(:migration) { described_class.new }
-
- let(:projects) { table(:projects) }
- let(:namespaces) { table(:namespaces) }
- let(:labels) { table(:labels) }
- let(:issues) { table(:issues) }
- let(:label_links) { table(:label_links) }
- let(:label_props) { IncidentManagement::CreateIncidentLabelService::LABEL_PROPERTIES }
-
- let(:namespace) { namespaces.create!(name: 'foo', path: 'foo') }
- let!(:project) { projects.create!(namespace_id: namespace.id) }
- let(:label) { labels.create!(project_id: project.id, **label_props) }
- let!(:incident_issue) { issues.create!(project_id: project.id) }
- let!(:other_issue) { issues.create!(project_id: project.id) }
-
- # Issue issue_type enum
- let(:issue_type) { 0 }
- let(:incident_type) { 1 }
-
- before do
- label_links.create!(target_id: incident_issue.id, label_id: label.id, target_type: 'Issue')
- end
-
- describe '#up' do
- it 'updates the incident issue type' do
- expect { migrate! }
- .to change { incident_issue.reload.issue_type }
- .from(issue_type)
- .to(incident_type)
-
- expect(other_issue.reload.issue_type).to eql(issue_type)
- end
- end
-
- describe '#down' do
- let!(:incident_issue) { issues.create!(project_id: project.id, issue_type: issue_type) }
-
- it 'updates the incident issue type' do
- migration.up
-
- expect { migration.down }
- .to change { incident_issue.reload.issue_type }
- .from(incident_type)
- .to(issue_type)
-
- expect(other_issue.reload.issue_type).to eql(issue_type)
- end
- end
-end
diff --git a/spec/migrations/migrate_merge_request_mentions_to_db_spec.rb b/spec/migrations/migrate_merge_request_mentions_to_db_spec.rb
deleted file mode 100644
index 06493c4e5c1..00000000000
--- a/spec/migrations/migrate_merge_request_mentions_to_db_spec.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe MigrateMergeRequestMentionsToDb, :migration do
- let(:users) { table(:users) }
- let(:projects) { table(:projects) }
- let(:namespaces) { table(:namespaces) }
- let(:merge_requests) { table(:merge_requests) }
- let(:merge_request_user_mentions) { table(:merge_request_user_mentions) }
-
- let(:user) { users.create!(name: 'root', email: 'root@example.com', username: 'root', projects_limit: 0) }
- let(:group) { namespaces.create!(name: 'group1', path: 'group1', owner_id: user.id, type: 'Group') }
- let(:project) { projects.create!(name: 'gitlab1', path: 'gitlab1', namespace_id: group.id, visibility_level: 0) }
-
- # migrateable resources
- let(:common_args) { { source_branch: 'master', source_project_id: project.id, target_project_id: project.id, author_id: user.id, description: 'mr description with @root mention' } }
- let!(:resource1) { merge_requests.create!(common_args.merge(title: "title 1", state_id: 1, target_branch: 'feature1')) }
- let!(:resource2) { merge_requests.create!(common_args.merge(title: "title 2", state_id: 1, target_branch: 'feature2')) }
- let!(:resource3) { merge_requests.create!(common_args.merge(title: "title 3", state_id: 1, target_branch: 'feature3')) }
-
- # non-migrateable resources
- # this merge request is already migrated, as it has a record in the merge_request_user_mentions table
- let!(:resource4) { merge_requests.create!(common_args.merge(title: "title 3", state_id: 1, target_branch: 'feature3')) }
- let!(:user_mention) { merge_request_user_mentions.create!(merge_request_id: resource4.id, mentioned_users_ids: [1]) }
-
- let!(:resource5) { merge_requests.create!(common_args.merge(title: "title 3", description: 'description with no mention', state_id: 1, target_branch: 'feature3')) }
-
- it_behaves_like 'schedules resource mentions migration', MergeRequest, false
-end
diff --git a/spec/migrations/migrate_store_security_reports_sidekiq_queue_spec.rb b/spec/migrations/migrate_store_security_reports_sidekiq_queue_spec.rb
deleted file mode 100644
index 35cb6104fe2..00000000000
--- a/spec/migrations/migrate_store_security_reports_sidekiq_queue_spec.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe MigrateStoreSecurityReportsSidekiqQueue, :redis do
- include Gitlab::Database::MigrationHelpers
- include StubWorker
-
- context 'when there are jobs in the queue' do
- it 'migrates queue when migrating up' do
- Sidekiq::Testing.disable! do
- stub_worker(queue: 'pipeline_default:store_security_reports').perform_async(1, 5)
-
- described_class.new.up
-
- expect(sidekiq_queue_length('pipeline_default:store_security_reports')).to eq 0
- expect(sidekiq_queue_length('security_scans:store_security_reports')).to eq 1
- end
- end
-
- it 'migrates queue when migrating down' do
- Sidekiq::Testing.disable! do
- stub_worker(queue: 'security_scans:store_security_reports').perform_async(1, 5)
-
- described_class.new.down
-
- expect(sidekiq_queue_length('pipeline_default:store_security_reports')).to eq 1
- expect(sidekiq_queue_length('security_scans:store_security_reports')).to eq 0
- end
- end
- end
-end
diff --git a/spec/migrations/migrate_sync_security_reports_to_report_approval_rules_sidekiq_queue_spec.rb b/spec/migrations/migrate_sync_security_reports_to_report_approval_rules_sidekiq_queue_spec.rb
deleted file mode 100644
index a9e386301b8..00000000000
--- a/spec/migrations/migrate_sync_security_reports_to_report_approval_rules_sidekiq_queue_spec.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe MigrateSyncSecurityReportsToReportApprovalRulesSidekiqQueue, :redis do
- include Gitlab::Database::MigrationHelpers
- include StubWorker
-
- context 'when there are jobs in the queue' do
- it 'migrates queue when migrating up' do
- Sidekiq::Testing.disable! do
- stub_worker(queue: 'pipeline_default:sync_security_reports_to_report_approval_rules').perform_async(1, 5)
-
- described_class.new.up
-
- expect(sidekiq_queue_length('pipeline_default:sync_security_reports_to_report_approval_rules')).to eq 0
- expect(sidekiq_queue_length('security_scans:sync_security_reports_to_report_approval_rules')).to eq 1
- end
- end
-
- it 'migrates queue when migrating down' do
- Sidekiq::Testing.disable! do
- stub_worker(queue: 'security_scans:sync_security_reports_to_report_approval_rules').perform_async(1, 5)
-
- described_class.new.down
-
- expect(sidekiq_queue_length('pipeline_default:sync_security_reports_to_report_approval_rules')).to eq 1
- expect(sidekiq_queue_length('security_scans:sync_security_reports_to_report_approval_rules')).to eq 0
- end
- end
- end
-end
diff --git a/spec/migrations/orphaned_invite_tokens_cleanup_spec.rb b/spec/migrations/orphaned_invite_tokens_cleanup_spec.rb
index be5e7756514..b33e29f82e2 100644
--- a/spec/migrations/orphaned_invite_tokens_cleanup_spec.rb
+++ b/spec/migrations/orphaned_invite_tokens_cleanup_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration! 'orphaned_invite_tokens_cleanup'
+require_migration!
RSpec.describe OrphanedInviteTokensCleanup, :migration do
def create_member(**extra_attributes)
diff --git a/spec/migrations/populate_remaining_missing_dismissal_information_for_vulnerabilities_spec.rb b/spec/migrations/populate_remaining_missing_dismissal_information_for_vulnerabilities_spec.rb
deleted file mode 100644
index 986436971ac..00000000000
--- a/spec/migrations/populate_remaining_missing_dismissal_information_for_vulnerabilities_spec.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe PopulateRemainingMissingDismissalInformationForVulnerabilities do
- let(:users) { table(:users) }
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:vulnerabilities) { table(:vulnerabilities) }
-
- let(:user) { users.create!(name: 'test', email: 'test@example.com', projects_limit: 5) }
- let(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') }
- let(:project) { projects.create!(namespace_id: namespace.id, name: 'foo') }
-
- let(:states) { { detected: 1, dismissed: 2, resolved: 3, confirmed: 4 } }
- let!(:vulnerability_1) { vulnerabilities.create!(title: 'title', state: states[:detected], severity: 0, confidence: 5, report_type: 2, project_id: project.id, author_id: user.id) }
- let!(:vulnerability_2) { vulnerabilities.create!(title: 'title', state: states[:dismissed], severity: 0, confidence: 5, report_type: 2, project_id: project.id, author_id: user.id) }
- let!(:vulnerability_3) { vulnerabilities.create!(title: 'title', state: states[:resolved], severity: 0, confidence: 5, report_type: 2, project_id: project.id, author_id: user.id) }
- let!(:vulnerability_4) { vulnerabilities.create!(title: 'title', state: states[:confirmed], severity: 0, confidence: 5, report_type: 2, project_id: project.id, author_id: user.id) }
-
- describe '#perform' do
- it 'calls the background migration class instance with broken vulnerability IDs' do
- expect_next_instance_of(::Gitlab::BackgroundMigration::PopulateMissingVulnerabilityDismissalInformation) do |migrator|
- expect(migrator).to receive(:perform).with(vulnerability_2.id)
- end
-
- migrate!
- end
- end
-end
diff --git a/spec/migrations/remove_additional_application_settings_rows_spec.rb b/spec/migrations/remove_additional_application_settings_rows_spec.rb
deleted file mode 100644
index d781195abf2..00000000000
--- a/spec/migrations/remove_additional_application_settings_rows_spec.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-RSpec.describe RemoveAdditionalApplicationSettingsRows do
- let(:application_settings) { table(:application_settings) }
-
- it 'removes additional rows from application settings' do
- 3.times { application_settings.create! }
- latest_settings = application_settings.create!
-
- disable_migrations_output { migrate! }
-
- expect(application_settings.count).to eq(1)
- expect(application_settings.first).to eq(latest_settings)
- end
-
- it 'leaves only row in application_settings' do
- latest_settings = application_settings.create!
-
- disable_migrations_output { migrate! }
-
- expect(application_settings.first).to eq(latest_settings)
- end
-end
diff --git a/spec/migrations/remove_deprecated_jenkins_service_records_spec.rb b/spec/migrations/remove_deprecated_jenkins_service_records_spec.rb
deleted file mode 100644
index 817cf183e0c..00000000000
--- a/spec/migrations/remove_deprecated_jenkins_service_records_spec.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-require_migration!('ensure_deprecated_jenkins_service_records_removal')
-
-RSpec.shared_examples 'remove DeprecatedJenkinsService records' do
- let(:services) { table(:services) }
-
- before do
- services.create!(type: 'JenkinsDeprecatedService')
- services.create!(type: 'JenkinsService')
- end
-
- it 'deletes services when template and attached to a project' do
- expect { migrate! }
- .to change { services.where(type: 'JenkinsDeprecatedService').count }.from(1).to(0)
- .and not_change { services.where(type: 'JenkinsService').count }
- end
-end
-
-RSpec.describe RemoveDeprecatedJenkinsServiceRecords, :migration do
- it_behaves_like 'remove DeprecatedJenkinsService records'
-end
-
-RSpec.describe EnsureDeprecatedJenkinsServiceRecordsRemoval, :migration do
- it_behaves_like 'remove DeprecatedJenkinsService records'
-end
diff --git a/spec/migrations/remove_duplicate_labels_from_groups_spec.rb b/spec/migrations/remove_duplicate_labels_from_groups_spec.rb
deleted file mode 100644
index 125314f70dd..00000000000
--- a/spec/migrations/remove_duplicate_labels_from_groups_spec.rb
+++ /dev/null
@@ -1,227 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!('remove_duplicate_labels_from_group')
-
-RSpec.describe RemoveDuplicateLabelsFromGroup do
- let(:labels_table) { table(:labels) }
- let(:labels) { labels_table.all }
- let(:projects_table) { table(:projects) }
- let(:projects) { projects_table.all }
- let(:namespaces_table) { table(:namespaces) }
- let(:namespaces) { namespaces_table.all }
- let(:backup_labels_table) { table(:backup_labels) }
- let(:backup_labels) { backup_labels_table.all }
- # for those cases where we can't use the activerecord class because the `type` column
- # makes it think it has polymorphism and should be/have a Label subclass
- let(:sql_backup_labels) { ApplicationRecord.connection.execute('SELECT * from backup_labels') }
-
- # all the possible tables with records that may have a relationship with a label
- let(:analytics_cycle_analytics_group_stages_table) { table(:analytics_cycle_analytics_group_stages) }
- let(:analytics_cycle_analytics_project_stages_table) { table(:analytics_cycle_analytics_project_stages) }
- let(:board_labels_table) { table(:board_labels) }
- let(:label_links_table) { table(:label_links) }
- let(:label_priorities_table) { table(:label_priorities) }
- let(:lists_table) { table(:lists) }
- let(:resource_label_events_table) { table(:resource_label_events) }
-
- let!(:group_one) { namespaces_table.create!(id: 1, type: 'Group', name: 'group', path: 'group') }
- let!(:project_one) do
- projects_table.create!(id: 1, name: 'project', path: 'project',
- visibility_level: 0, namespace_id: group_one.id)
- end
-
- let(:label_title) { 'bug' }
- let(:label_color) { 'red' }
- let(:label_description) { 'nice label' }
- let(:project_id) { project_one.id }
- let(:group_id) { group_one.id }
- let(:other_title) { 'feature' }
-
- let(:group_label_attributes) do
- {
- title: label_title, color: label_color, group_id: group_id, type: 'GroupLabel', template: false, description: label_description
- }
- end
-
- let(:migration) { described_class.new }
-
- describe 'removing full duplicates' do
- context 'when there are no duplicate labels' do
- let!(:first_label) { labels_table.create!(group_label_attributes.merge(id: 1, title: "a different label")) }
- let!(:second_label) { labels_table.create!(group_label_attributes.merge(id: 2, title: "a totally different label")) }
-
- it 'does not remove anything' do
- expect { migration.up }.not_to change { backup_labels_table.count }
- end
-
- it 'restores removed records when rolling back - no change' do
- migration.up
-
- expect { migration.down }.not_to change { labels_table.count }
- end
- end
-
- context 'with duplicates with no relationships' do
- let!(:first_label) { labels_table.create!(group_label_attributes.merge(id: 1)) }
- let!(:second_label) { labels_table.create!(group_label_attributes.merge(id: 2)) }
- let!(:third_label) { labels_table.create!(group_label_attributes.merge(id: 3, title: other_title)) }
- let!(:fourth_label) { labels_table.create!(group_label_attributes.merge(id: 4, title: other_title)) }
-
- it 'creates a backup record for each removed record' do
- expect { migration.up }.to change { backup_labels_table.count }.from(0).to(2)
- end
-
- it 'creates the correct backup records with `create` restore_action' do
- migration.up
-
- expect(sql_backup_labels.find { |bl| bl["id"] == 2 }).to include(second_label.attributes.merge("restore_action" => described_class::CREATE, "new_title" => nil, "created_at" => anything, "updated_at" => anything))
- expect(sql_backup_labels.find { |bl| bl["id"] == 4 }).to include(fourth_label.attributes.merge("restore_action" => described_class::CREATE, "new_title" => nil, "created_at" => anything, "updated_at" => anything))
- end
-
- it 'deletes all but one' do
- migration.up
-
- expect { second_label.reload }.to raise_error(ActiveRecord::RecordNotFound)
- expect { fourth_label.reload }.to raise_error(ActiveRecord::RecordNotFound)
- end
-
- it 'restores removed records on rollback' do
- second_label_attributes = modified_attributes(second_label)
- fourth_label_attributes = modified_attributes(fourth_label)
-
- migration.up
-
- migration.down
-
- expect(second_label.attributes).to include(second_label_attributes)
- expect(fourth_label.attributes).to include(fourth_label_attributes)
- end
- end
-
- context 'two duplicate records, one of which has a relationship' do
- let!(:first_label) { labels_table.create!(group_label_attributes.merge(id: 1)) }
- let!(:second_label) { labels_table.create!(group_label_attributes.merge(id: 2)) }
- let!(:label_priority) { label_priorities_table.create!(label_id: second_label.id, project_id: project_id, priority: 1) }
-
- it 'does not remove anything' do
- expect { migration.up }.not_to change { labels_table.count }
- end
-
- it 'does not create a backup record with `create` restore_action' do
- expect { migration.up }.not_to change { backup_labels_table.where(restore_action: described_class::CREATE).count }
- end
-
- it 'restores removed records when rolling back - no change' do
- migration.up
-
- expect { migration.down }.not_to change { labels_table.count }
- end
- end
-
- context 'multiple duplicates, a subset of which have relationships' do
- let!(:first_label) { labels_table.create!(group_label_attributes.merge(id: 1)) }
- let!(:second_label) { labels_table.create!(group_label_attributes.merge(id: 2)) }
- let!(:label_priority_for_second_label) { label_priorities_table.create!(label_id: second_label.id, project_id: project_id, priority: 1) }
- let!(:third_label) { labels_table.create!(group_label_attributes.merge(id: 3)) }
- let!(:fourth_label) { labels_table.create!(group_label_attributes.merge(id: 4)) }
- let!(:label_priority_for_fourth_label) { label_priorities_table.create!(label_id: fourth_label.id, project_id: project_id, priority: 2) }
-
- it 'creates a backup record with `create` restore_action for each removed record' do
- expect { migration.up }.to change { backup_labels_table.where(restore_action: described_class::CREATE).count }.from(0).to(1)
- end
-
- it 'creates the correct backup records' do
- migration.up
-
- expect(sql_backup_labels.find { |bl| bl["id"] == 3 }).to include(third_label.attributes.merge("restore_action" => described_class::CREATE, "new_title" => nil, "created_at" => anything, "updated_at" => anything))
- end
-
- it 'deletes the duplicate record' do
- migration.up
-
- expect { first_label.reload }.not_to raise_error
- expect { second_label.reload }.not_to raise_error
- expect { third_label.reload }.to raise_error(ActiveRecord::RecordNotFound)
- end
-
- it 'restores removed records on rollback' do
- third_label_attributes = modified_attributes(third_label)
-
- migration.up
- migration.down
-
- expect(third_label.attributes).to include(third_label_attributes)
- end
- end
- end
-
- describe 'renaming partial duplicates' do
- # partial duplicates - only group_id and title match. Distinct colour prevents deletion.
- context 'when there are no duplicate labels' do
- let!(:first_label) { labels_table.create!(group_label_attributes.merge(id: 1, title: "a unique label", color: 'green')) }
- let!(:second_label) { labels_table.create!(group_label_attributes.merge(id: 2, title: "a totally different, unique, label", color: 'blue')) }
-
- it 'does not rename anything' do
- expect { migration.up }.not_to change { backup_labels_table.count }
- end
- end
-
- context 'with duplicates with no relationships' do
- let!(:first_label) { labels_table.create!(group_label_attributes.merge(id: 1, color: 'green')) }
- let!(:second_label) { labels_table.create!(group_label_attributes.merge(id: 2, color: 'blue')) }
- let!(:third_label) { labels_table.create!(group_label_attributes.merge(id: 3, title: other_title, color: 'purple')) }
- let!(:fourth_label) { labels_table.create!(group_label_attributes.merge(id: 4, title: other_title, color: 'yellow')) }
-
- it 'creates a backup record for each renamed record' do
- expect { migration.up }.to change { backup_labels_table.count }.from(0).to(2)
- end
-
- it 'creates the correct backup records with `rename` restore_action' do
- migration.up
-
- expect(sql_backup_labels.find { |bl| bl["id"] == 2 }).to include(second_label.attributes.merge("restore_action" => described_class::RENAME, "created_at" => anything, "updated_at" => anything))
- expect(sql_backup_labels.find { |bl| bl["id"] == 4 }).to include(fourth_label.attributes.merge("restore_action" => described_class::RENAME, "created_at" => anything, "updated_at" => anything))
- end
-
- it 'modifies the titles of the partial duplicates' do
- migration.up
-
- expect(second_label.reload.title).to match(/#{label_title}_duplicate#{second_label.id}$/)
- expect(fourth_label.reload.title).to match(/#{other_title}_duplicate#{fourth_label.id}$/)
- end
-
- it 'restores renamed records on rollback' do
- second_label_attributes = modified_attributes(second_label)
- fourth_label_attributes = modified_attributes(fourth_label)
-
- migration.up
-
- migration.down
-
- expect(second_label.reload.attributes).to include(second_label_attributes)
- expect(fourth_label.reload.attributes).to include(fourth_label_attributes)
- end
-
- context 'when the labels have a long title that might overflow' do
- let(:long_title) { "a" * 255 }
-
- before do
- first_label.update_attribute(:title, long_title)
- second_label.update_attribute(:title, long_title)
- end
-
- it 'keeps the length within the limit' do
- migration.up
-
- expect(second_label.reload.title).to eq("#{"a" * 244}_duplicate#{second_label.id}")
- expect(second_label.title.length).to eq(255)
- end
- end
- end
- end
-
- def modified_attributes(label)
- label.attributes.except('created_at', 'updated_at')
- end
-end
diff --git a/spec/migrations/remove_duplicate_labels_from_project_spec.rb b/spec/migrations/remove_duplicate_labels_from_project_spec.rb
deleted file mode 100644
index eeb9f155e01..00000000000
--- a/spec/migrations/remove_duplicate_labels_from_project_spec.rb
+++ /dev/null
@@ -1,239 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe RemoveDuplicateLabelsFromProject do
- let(:labels_table) { table(:labels) }
- let(:labels) { labels_table.all }
- let(:projects_table) { table(:projects) }
- let(:projects) { projects_table.all }
- let(:namespaces_table) { table(:namespaces) }
- let(:namespaces) { namespaces_table.all }
- let(:backup_labels_table) { table(:backup_labels) }
- let(:backup_labels) { backup_labels_table.all }
-
- # all the possible tables with records that may have a relationship with a label
- let(:analytics_cycle_analytics_group_stages_table) { table(:analytics_cycle_analytics_group_stages) }
- let(:analytics_cycle_analytics_project_stages_table) { table(:analytics_cycle_analytics_project_stages) }
- let(:board_labels_table) { table(:board_labels) }
- let(:label_links_table) { table(:label_links) }
- let(:label_priorities_table) { table(:label_priorities) }
- let(:lists_table) { table(:lists) }
- let(:resource_label_events_table) { table(:resource_label_events) }
-
- let!(:group_one) { namespaces_table.create!(id: 1, type: 'Group', name: 'group', path: 'group') }
- let!(:project_one) do
- projects_table.create!(id: 1, name: 'project', path: 'project',
- visibility_level: 0, namespace_id: group_one.id)
- end
-
- let(:label_title) { 'bug' }
- let(:label_color) { 'red' }
- let(:label_description) { 'nice label' }
- let(:group_id) { group_one.id }
- let(:project_id) { project_one.id }
- let(:other_title) { 'feature' }
-
- let(:group_label_attributes) do
- {
- title: label_title, color: label_color, group_id: group_id, type: 'GroupLabel', template: false, description: label_description
- }
- end
-
- let(:project_label_attributes) do
- {
- title: label_title, color: label_color, project_id: project_id, type: 'ProjectLabel', template: false, description: label_description
- }
- end
-
- let(:migration) { described_class.new }
-
- describe 'removing full duplicates' do
- context 'when there are no duplicate labels' do
- let!(:first_label) { labels_table.create!(project_label_attributes.merge(id: 1, title: "a different label")) }
- let!(:second_label) { labels_table.create!(project_label_attributes.merge(id: 2, title: "a totally different label")) }
-
- it 'does not remove anything' do
- expect { migration.up }.not_to change { backup_labels_table.count }
- end
-
- it 'restores removed records when rolling back - no change' do
- migration.up
-
- expect { migration.down }.not_to change { labels_table.count }
- end
- end
-
- context 'with duplicates with no relationships' do
- # can't use the activerecord class because the `type` makes it think it has polymorphism and should be/have a ProjectLabel subclass
- let(:backup_labels) { ApplicationRecord.connection.execute('SELECT * from backup_labels') }
-
- let!(:first_label) { labels_table.create!(project_label_attributes.merge(id: 1)) }
- let!(:second_label) { labels_table.create!(project_label_attributes.merge(id: 2)) }
- let!(:third_label) { labels_table.create!(project_label_attributes.merge(id: 3, title: other_title)) }
- let!(:fourth_label) { labels_table.create!(project_label_attributes.merge(id: 4, title: other_title)) }
-
- it 'creates a backup record for each removed record' do
- expect { migration.up }.to change { backup_labels_table.count }.from(0).to(2)
- end
-
- it 'creates the correct backup records with `create` restore_action' do
- migration.up
-
- expect(backup_labels.find { |bl| bl["id"] == 2 }).to include(second_label.attributes.merge("restore_action" => described_class::CREATE, "new_title" => nil, "created_at" => anything, "updated_at" => anything))
- expect(backup_labels.find { |bl| bl["id"] == 4 }).to include(fourth_label.attributes.merge("restore_action" => described_class::CREATE, "new_title" => nil, "created_at" => anything, "updated_at" => anything))
- end
-
- it 'deletes all but one' do
- migration.up
-
- expect { second_label.reload }.to raise_error(ActiveRecord::RecordNotFound)
- expect { fourth_label.reload }.to raise_error(ActiveRecord::RecordNotFound)
- end
-
- it 'restores removed records on rollback' do
- second_label_attributes = modified_attributes(second_label)
- fourth_label_attributes = modified_attributes(fourth_label)
-
- migration.up
-
- migration.down
-
- expect(second_label.attributes).to include(second_label_attributes)
- expect(fourth_label.attributes).to include(fourth_label_attributes)
- end
- end
-
- context 'two duplicate records, one of which has a relationship' do
- let!(:first_label) { labels_table.create!(project_label_attributes.merge(id: 1)) }
- let!(:second_label) { labels_table.create!(project_label_attributes.merge(id: 2)) }
- let!(:label_priority) { label_priorities_table.create!(label_id: second_label.id, project_id: project_id, priority: 1) }
-
- it 'does not remove anything' do
- expect { migration.up }.not_to change { labels_table.count }
- end
-
- it 'does not create a backup record with `create` restore_action' do
- expect { migration.up }.not_to change { backup_labels_table.where(restore_action: described_class::CREATE).count }
- end
-
- it 'restores removed records when rolling back - no change' do
- migration.up
-
- expect { migration.down }.not_to change { labels_table.count }
- end
- end
-
- context 'multiple duplicates, a subset of which have relationships' do
- let!(:first_label) { labels_table.create!(project_label_attributes.merge(id: 1)) }
- let!(:second_label) { labels_table.create!(project_label_attributes.merge(id: 2)) }
- let!(:label_priority_for_second_label) { label_priorities_table.create!(label_id: second_label.id, project_id: project_id, priority: 1) }
- let!(:third_label) { labels_table.create!(project_label_attributes.merge(id: 3)) }
- let!(:fourth_label) { labels_table.create!(project_label_attributes.merge(id: 4)) }
- let!(:label_priority_for_fourth_label) { label_priorities_table.create!(label_id: fourth_label.id, project_id: project_id, priority: 2) }
-
- it 'creates a backup record with `create` restore_action for each removed record' do
- expect { migration.up }.to change { backup_labels_table.where(restore_action: described_class::CREATE).count }.from(0).to(1)
- end
-
- it 'creates the correct backup records' do
- migration.up
-
- # can't use the activerecord class because the `type` column makes it think it has polymorphism and should be/have a ProjectLabel subclass
- backup_labels = ApplicationRecord.connection.execute('SELECT * from backup_labels')
-
- expect(backup_labels.find { |bl| bl["id"] == 3 }).to include(third_label.attributes.merge("restore_action" => described_class::CREATE, "new_title" => nil, "created_at" => anything, "updated_at" => anything))
- end
-
- it 'deletes the duplicate record' do
- migration.up
-
- expect { first_label.reload }.not_to raise_error
- expect { second_label.reload }.not_to raise_error
- expect { third_label.reload }.to raise_error(ActiveRecord::RecordNotFound)
- end
-
- it 'restores removed records on rollback' do
- third_label_attributes = modified_attributes(third_label)
-
- migration.up
- migration.down
-
- expect(third_label.attributes).to include(third_label_attributes)
- end
- end
- end
-
- describe 'renaming partial duplicates' do
- # partial duplicates - only project_id and title match. Distinct colour prevents deletion.
- context 'when there are no duplicate labels' do
- let!(:first_label) { labels_table.create!(project_label_attributes.merge(id: 1, title: "a unique label", color: 'green')) }
- let!(:second_label) { labels_table.create!(project_label_attributes.merge(id: 2, title: "a totally different, unique, label", color: 'blue')) }
-
- it 'does not rename anything' do
- expect { migration.up }.not_to change { backup_labels_table.count }
- end
- end
-
- context 'with duplicates with no relationships' do
- let!(:first_label) { labels_table.create!(project_label_attributes.merge(id: 1, color: 'green')) }
- let!(:second_label) { labels_table.create!(project_label_attributes.merge(id: 2, color: 'blue')) }
- let!(:third_label) { labels_table.create!(project_label_attributes.merge(id: 3, title: other_title, color: 'purple')) }
- let!(:fourth_label) { labels_table.create!(project_label_attributes.merge(id: 4, title: other_title, color: 'yellow')) }
-
- it 'creates a backup record for each renamed record' do
- expect { migration.up }.to change { backup_labels_table.count }.from(0).to(2)
- end
-
- it 'creates the correct backup records with `rename` restore_action' do
- migration.up
-
- # can't use the activerecord class because the `type` makes it think it has polymorphism and should be/have a ProjectLabel subclass
- backup_labels = ApplicationRecord.connection.execute('SELECT * from backup_labels')
-
- expect(backup_labels.find { |bl| bl["id"] == 2 }).to include(second_label.attributes.merge("restore_action" => described_class::RENAME, "created_at" => anything, "updated_at" => anything))
- expect(backup_labels.find { |bl| bl["id"] == 4 }).to include(fourth_label.attributes.merge("restore_action" => described_class::RENAME, "created_at" => anything, "updated_at" => anything))
- end
-
- it 'modifies the titles of the partial duplicates' do
- migration.up
-
- expect(second_label.reload.title).to match(/#{label_title}_duplicate#{second_label.id}$/)
- expect(fourth_label.reload.title).to match(/#{other_title}_duplicate#{fourth_label.id}$/)
- end
-
- it 'restores renamed records on rollback' do
- second_label_attributes = modified_attributes(second_label)
- fourth_label_attributes = modified_attributes(fourth_label)
-
- migration.up
-
- migration.down
-
- expect(second_label.reload.attributes).to include(second_label_attributes)
- expect(fourth_label.reload.attributes).to include(fourth_label_attributes)
- end
-
- context 'when the labels have a long title that might overflow' do
- let(:long_title) { "a" * 255 }
-
- before do
- first_label.update_attribute(:title, long_title)
- second_label.update_attribute(:title, long_title)
- end
-
- it 'keeps the length within the limit' do
- migration.up
-
- expect(second_label.reload.title).to eq("#{"a" * 244}_duplicate#{second_label.id}")
- expect(second_label.title.length).to eq 255
- end
- end
- end
- end
-
- def modified_attributes(label)
- label.attributes.except('created_at', 'updated_at')
- end
-end
diff --git a/spec/migrations/remove_gitlab_issue_tracker_service_records_spec.rb b/spec/migrations/remove_gitlab_issue_tracker_service_records_spec.rb
deleted file mode 100644
index b4aa5187d4c..00000000000
--- a/spec/migrations/remove_gitlab_issue_tracker_service_records_spec.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe RemoveGitlabIssueTrackerServiceRecords do
- let(:services) { table(:services) }
-
- before do
- 5.times { services.create!(type: 'GitlabIssueTrackerService') }
- services.create!(type: 'SomeOtherType')
- end
-
- it 'removes services records of type GitlabIssueTrackerService', :aggregate_failures do
- expect { migrate! }.to change { services.count }.from(6).to(1)
- expect(services.first.type).to eq('SomeOtherType')
- expect(services.where(type: 'GitlabIssueTrackerService')).to be_empty
- end
-end
diff --git a/spec/migrations/remove_orphan_service_hooks_spec.rb b/spec/migrations/remove_orphan_service_hooks_spec.rb
deleted file mode 100644
index 71e70daf1e6..00000000000
--- a/spec/migrations/remove_orphan_service_hooks_spec.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-require_migration!('add_web_hooks_service_foreign_key')
-
-RSpec.describe RemoveOrphanServiceHooks, schema: 20201203123201 do
- let(:web_hooks) { table(:web_hooks) }
- let(:services) { table(:services) }
-
- before do
- services.create!
- web_hooks.create!(service_id: services.first.id, type: 'ServiceHook')
- web_hooks.create!(service_id: nil)
-
- AddWebHooksServiceForeignKey.new.down
- web_hooks.create!(service_id: non_existing_record_id, type: 'ServiceHook')
- AddWebHooksServiceForeignKey.new.up
- end
-
- it 'removes service hooks where the referenced service does not exist', :aggregate_failures do
- expect { RemoveOrphanServiceHooks.new.up }.to change { web_hooks.count }.by(-1)
- expect(web_hooks.where.not(service_id: services.select(:id)).count).to eq(0)
- end
-end
diff --git a/spec/migrations/remove_orphaned_invited_members_spec.rb b/spec/migrations/remove_orphaned_invited_members_spec.rb
deleted file mode 100644
index 67e98b69ccc..00000000000
--- a/spec/migrations/remove_orphaned_invited_members_spec.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe RemoveOrphanedInvitedMembers do
- let(:members_table) { table(:members) }
- let(:users_table) { table(:users) }
- let(:namespaces_table) { table(:namespaces) }
- let(:projects_table) { table(:projects) }
-
- let!(:user1) { users_table.create!(name: 'user1', email: 'user1@example.com', projects_limit: 1) }
- let!(:user2) { users_table.create!(name: 'user2', email: 'user2@example.com', projects_limit: 1) }
- let!(:group) { namespaces_table.create!(type: 'Group', name: 'group', path: 'group') }
- let!(:project) { projects_table.create!(name: 'project', path: 'project', namespace_id: group.id) }
-
- let!(:member1) { create_member(user_id: user1.id, source_type: 'Project', source_id: project.id, access_level: 10) }
- let!(:member2) { create_member(user_id: user2.id, source_type: 'Group', source_id: group.id, access_level: 20) }
-
- let!(:invited_member1) do
- create_member(user_id: nil, source_type: 'Project', source_id: project.id,
- invite_token: SecureRandom.hex, invite_accepted_at: Time.now,
- access_level: 20)
- end
-
- let!(:invited_member2) do
- create_member(user_id: nil, source_type: 'Group', source_id: group.id,
- invite_token: SecureRandom.hex, invite_accepted_at: Time.now,
- access_level: 20)
- end
-
- let!(:orphaned_member1) do
- create_member(user_id: nil, source_type: 'Project', source_id: project.id,
- invite_accepted_at: Time.now, access_level: 30)
- end
-
- let!(:orphaned_member2) do
- create_member(user_id: nil, source_type: 'Group', source_id: group.id,
- invite_accepted_at: Time.now, access_level: 20)
- end
-
- it 'removes orphaned invited members but keeps current members' do
- expect { migrate! }.to change { members_table.count }.from(6).to(4)
-
- expect(members_table.all.pluck(:id)).to contain_exactly(member1.id, member2.id, invited_member1.id, invited_member2.id)
- end
-
- def create_member(options)
- members_table.create!(
- {
- notification_level: 0,
- ldap: false,
- override: false
- }.merge(options)
- )
- end
-end
diff --git a/spec/migrations/remove_packages_deprecated_dependencies_spec.rb b/spec/migrations/remove_packages_deprecated_dependencies_spec.rb
deleted file mode 100644
index f76a26bcdc1..00000000000
--- a/spec/migrations/remove_packages_deprecated_dependencies_spec.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe RemovePackagesDeprecatedDependencies do
- let(:projects) { table(:projects) }
- let(:packages) { table(:packages_packages) }
- let(:dependency_links) { table(:packages_dependency_links) }
- let(:dependencies) { table(:packages_dependencies) }
-
- before do
- projects.create!(id: 123, name: 'gitlab', path: 'gitlab-org/gitlab-ce', namespace_id: 1)
- packages.create!(id: 1, name: 'package', version: '1.0.0', package_type: 4, project_id: 123)
- 5.times do |i|
- dependencies.create!(id: i, name: "pkg_dependency_#{i}", version_pattern: '~1.0.0')
- dependency_links.create!(package_id: 1, dependency_id: i, dependency_type: 5)
- end
- dependencies.create!(id: 10, name: 'valid_pkg_dependency', version_pattern: '~2.5.0')
- dependency_links.create!(package_id: 1, dependency_id: 10, dependency_type: 1)
- end
-
- it 'removes all dependency links with type 5' do
- expect(dependency_links.count).to eq 6
-
- migrate!
-
- expect(dependency_links.count).to eq 1
- end
-end
diff --git a/spec/migrations/remove_security_dashboard_feature_flag_spec.rb b/spec/migrations/remove_security_dashboard_feature_flag_spec.rb
deleted file mode 100644
index fea7fe01cc7..00000000000
--- a/spec/migrations/remove_security_dashboard_feature_flag_spec.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-RSpec.describe RemoveSecurityDashboardFeatureFlag do
- let(:feature_gates) { table(:feature_gates) }
-
- subject(:migration) { described_class.new }
-
- describe '#up' do
- it 'deletes the security_dashboard feature gate' do
- security_dashboard_feature = feature_gates.create!(feature_key: :security_dashboard, key: :boolean, value: 'false')
- actors_security_dashboard_feature = feature_gates.create!(feature_key: :security_dashboard, key: :actors, value: 'Project:1')
-
- migration.up
-
- expect { security_dashboard_feature.reload }.to raise_error(ActiveRecord::RecordNotFound)
- expect(actors_security_dashboard_feature.reload).to be_present
- end
- end
-
- describe '#down' do
- it 'copies the instance_security_dashboard feature gate to a security_dashboard gate' do
- feature_gates.create!(feature_key: :instance_security_dashboard, key: :actors, value: 'Project:1')
- feature_gates.create!(feature_key: :instance_security_dashboard, key: 'boolean', value: 'false')
-
- migration.down
-
- security_dashboard_feature = feature_gates.find_by(feature_key: :security_dashboard, key: :boolean)
- expect(security_dashboard_feature.value).to eq('false')
- end
-
- context 'when there is no instance_security_dashboard gate' do
- it 'does nothing' do
- migration.down
-
- security_dashboard_feature = feature_gates.find_by(feature_key: :security_dashboard, key: :boolean)
- expect(security_dashboard_feature).to be_nil
- end
- end
-
- context 'when there already is a security_dashboard gate' do
- it 'does nothing' do
- feature_gates.create!(feature_key: :security_dashboard, key: 'boolean', value: 'false')
- feature_gates.create!(feature_key: :instance_security_dashboard, key: 'boolean', value: 'false')
-
- expect { migration.down }.not_to raise_error
- end
- end
- end
-end
diff --git a/spec/migrations/rename_security_dashboard_feature_flag_to_instance_security_dashboard_spec.rb b/spec/migrations/rename_security_dashboard_feature_flag_to_instance_security_dashboard_spec.rb
deleted file mode 100644
index fcbf94812fb..00000000000
--- a/spec/migrations/rename_security_dashboard_feature_flag_to_instance_security_dashboard_spec.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-RSpec.describe RenameSecurityDashboardFeatureFlagToInstanceSecurityDashboard do
- let(:feature_gates) { table(:feature_gates) }
-
- subject(:migration) { described_class.new }
-
- describe '#up' do
- it 'copies the security_dashboard feature gate to a new instance_security_dashboard gate' do
- feature_gates.create!(feature_key: :security_dashboard, key: :actors, value: 'Project:1')
- feature_gates.create!(feature_key: :security_dashboard, key: :boolean, value: 'false')
-
- migration.up
-
- instance_security_dashboard_feature = feature_gates.find_by(feature_key: :instance_security_dashboard, key: :boolean)
- expect(instance_security_dashboard_feature.value).to eq('false')
- end
-
- context 'when there is no security_dashboard gate' do
- it 'does nothing' do
- migration.up
-
- instance_security_dashboard_feature = feature_gates.find_by(feature_key: :instance_security_dashboard, key: :boolean)
- expect(instance_security_dashboard_feature).to be_nil
- end
- end
-
- context 'when there is already an instance_security_dashboard gate' do
- it 'does nothing' do
- feature_gates.create!(feature_key: :security_dashboard, key: 'boolean', value: 'false')
- feature_gates.create!(feature_key: :instance_security_dashboard, key: 'boolean', value: 'false')
-
- expect { migration.up }.not_to raise_error
- end
- end
- end
-
- describe '#down' do
- it 'removes the instance_security_dashboard gate' do
- actors_instance_security_dashboard_feature = feature_gates.create!(feature_key: :instance_security_dashboard, key: :actors, value: 'Project:1')
- instance_security_dashboard_feature = feature_gates.create!(feature_key: :instance_security_dashboard, key: :boolean, value: 'false')
-
- migration.down
-
- expect { instance_security_dashboard_feature.reload }.to raise_error(ActiveRecord::RecordNotFound)
- expect(actors_instance_security_dashboard_feature.reload).to be_present
- end
- end
-end
diff --git a/spec/migrations/rename_sitemap_namespace_spec.rb b/spec/migrations/rename_sitemap_namespace_spec.rb
deleted file mode 100644
index 21b74587d50..00000000000
--- a/spec/migrations/rename_sitemap_namespace_spec.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe RenameSitemapNamespace do
- let(:namespaces) { table(:namespaces) }
- let(:routes) { table(:routes) }
- let(:sitemap_path) { 'sitemap' }
-
- it 'correctly run #up and #down' do
- create_namespace(sitemap_path)
-
- reversible_migration do |migration|
- migration.before -> {
- expect(namespaces.pluck(:path)).to contain_exactly(sitemap_path)
- }
-
- migration.after -> {
- expect(namespaces.pluck(:path)).to contain_exactly(sitemap_path + '0')
- }
- end
- end
-
- def create_namespace(path)
- namespaces.create!(name: path, path: path).tap do |namespace|
- routes.create!(path: namespace.path, name: namespace.name, source_id: namespace.id, source_type: 'Namespace')
- end
- end
-end
diff --git a/spec/migrations/rename_sitemap_root_namespaces_spec.rb b/spec/migrations/rename_sitemap_root_namespaces_spec.rb
deleted file mode 100644
index 12a687194e0..00000000000
--- a/spec/migrations/rename_sitemap_root_namespaces_spec.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe RenameSitemapRootNamespaces do
- let(:namespaces) { table(:namespaces) }
- let(:routes) { table(:routes) }
- let(:sitemap_path) { 'sitemap.xml' }
- let(:sitemap_gz_path) { 'sitemap.xml.gz' }
- let(:other_path1) { 'sitemap.xmlfoo' }
- let(:other_path2) { 'foositemap.xml' }
-
- it 'correctly run #up and #down' do
- create_namespace(sitemap_path)
- create_namespace(sitemap_gz_path)
- create_namespace(other_path1)
- create_namespace(other_path2)
-
- reversible_migration do |migration|
- migration.before -> {
- expect(namespaces.pluck(:path)).to contain_exactly(sitemap_path, sitemap_gz_path, other_path1, other_path2)
- }
-
- migration.after -> {
- expect(namespaces.pluck(:path)).to contain_exactly(sitemap_path + '0', sitemap_gz_path + '0', other_path1, other_path2)
- }
- end
- end
-
- def create_namespace(path)
- namespaces.create!(name: path, path: path).tap do |namespace|
- routes.create!(path: namespace.path, name: namespace.name, source_id: namespace.id, source_type: 'Namespace')
- end
- end
-end
diff --git a/spec/migrations/reschedule_set_default_iteration_cadences_spec.rb b/spec/migrations/reschedule_set_default_iteration_cadences_spec.rb
deleted file mode 100644
index fb629c90d9f..00000000000
--- a/spec/migrations/reschedule_set_default_iteration_cadences_spec.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe RescheduleSetDefaultIterationCadences do
- let(:namespaces) { table(:namespaces) }
- let(:iterations) { table(:sprints) }
-
- let(:group_1) { namespaces.create!(name: 'test_1', path: 'test_1') }
- let!(:group_2) { namespaces.create!(name: 'test_2', path: 'test_2') }
- let(:group_3) { namespaces.create!(name: 'test_3', path: 'test_3') }
- let(:group_4) { namespaces.create!(name: 'test_4', path: 'test_4') }
- let(:group_5) { namespaces.create!(name: 'test_5', path: 'test_5') }
- let(:group_6) { namespaces.create!(name: 'test_6', path: 'test_6') }
- let(:group_7) { namespaces.create!(name: 'test_7', path: 'test_7') }
- let(:group_8) { namespaces.create!(name: 'test_8', path: 'test_8') }
-
- let!(:iteration_1) { iterations.create!(iid: 1, title: 'iteration 1', group_id: group_1.id, start_date: 2.days.from_now, due_date: 3.days.from_now) }
- let!(:iteration_2) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_3.id, start_date: 2.days.from_now, due_date: 3.days.from_now) }
- let!(:iteration_3) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_4.id, start_date: 2.days.from_now, due_date: 3.days.from_now) }
- let!(:iteration_4) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_5.id, start_date: 2.days.from_now, due_date: 3.days.from_now) }
- let!(:iteration_5) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_6.id, start_date: 2.days.from_now, due_date: 3.days.from_now) }
- let!(:iteration_6) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_7.id, start_date: 2.days.from_now, due_date: 3.days.from_now) }
- let!(:iteration_7) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_8.id, start_date: 2.days.from_now, due_date: 3.days.from_now) }
-
- around do |example|
- freeze_time { Sidekiq::Testing.fake! { example.run } }
- end
-
- it 'schedules the background jobs', :aggregate_failures do
- stub_const("#{described_class.name}::BATCH_SIZE", 3)
-
- migrate!
-
- expect(BackgroundMigrationWorker.jobs.size).to be(3)
- expect(described_class::MIGRATION_CLASS).to be_scheduled_delayed_migration(2.minutes, group_1.id, group_3.id, group_4.id)
- expect(described_class::MIGRATION_CLASS).to be_scheduled_delayed_migration(4.minutes, group_5.id, group_6.id, group_7.id)
- expect(described_class::MIGRATION_CLASS).to be_scheduled_delayed_migration(6.minutes, group_8.id)
- end
-end
diff --git a/spec/migrations/reseed_merge_trains_enabled_spec.rb b/spec/migrations/reseed_merge_trains_enabled_spec.rb
deleted file mode 100644
index 14ed44151d3..00000000000
--- a/spec/migrations/reseed_merge_trains_enabled_spec.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe ReseedMergeTrainsEnabled do
- describe 'migrate' do
- let(:project_ci_cd_settings) { table(:project_ci_cd_settings) }
- let(:projects) { table(:projects) }
- let(:namespaces) { table(:namespaces) }
-
- context 'when on Gitlab.com' do
- before do
- namespace = namespaces.create!(name: 'hello', path: 'hello/')
- project1 = projects.create!(namespace_id: namespace.id)
- project2 = projects.create!(namespace_id: namespace.id)
- project_ci_cd_settings.create!(project_id: project1.id, merge_pipelines_enabled: true)
- project_ci_cd_settings.create!(project_id: project2.id, merge_pipelines_enabled: false)
- end
-
- it 'updates merge_trains_enabled to true for where merge_pipelines_enabled is true' do
- expect { migrate! }.to change(project_ci_cd_settings.where(merge_trains_enabled: true), :count).by(1)
- end
- end
- end
-end
diff --git a/spec/migrations/reseed_repository_storages_weighted_spec.rb b/spec/migrations/reseed_repository_storages_weighted_spec.rb
deleted file mode 100644
index d7efff3dfba..00000000000
--- a/spec/migrations/reseed_repository_storages_weighted_spec.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe ReseedRepositoryStoragesWeighted do
- let(:storages) { { "foo" => {}, "baz" => {} } }
- let(:application_settings) do
- table(:application_settings).tap do |klass|
- klass.class_eval do
- serialize :repository_storages
- end
- end
- end
-
- before do
- allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
- end
-
- let(:repository_storages) { ["foo"] }
- let!(:application_setting) { application_settings.create!(repository_storages: repository_storages) }
-
- context 'with empty repository_storages_weighted column' do
- it 'populates repository_storages_weighted properly' do
- migrate!
-
- expect(application_settings.find(application_setting.id).repository_storages_weighted).to eq({ "foo" => 100, "baz" => 0 })
- end
- end
-
- context 'with already-populated repository_storages_weighted column' do
- let(:existing_weights) { { "foo" => 100, "baz" => 50 } }
-
- it 'does not change repository_storages_weighted properly' do
- application_setting.repository_storages_weighted = existing_weights
- application_setting.save!
-
- migrate!
-
- expect(application_settings.find(application_setting.id).repository_storages_weighted).to eq(existing_weights)
- end
- end
-end
diff --git a/spec/migrations/save_instance_administrators_group_id_spec.rb b/spec/migrations/save_instance_administrators_group_id_spec.rb
deleted file mode 100644
index 0846df18b5e..00000000000
--- a/spec/migrations/save_instance_administrators_group_id_spec.rb
+++ /dev/null
@@ -1,99 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe SaveInstanceAdministratorsGroupId do
- let(:application_settings_table) { table(:application_settings) }
-
- let(:instance_administrators_group) do
- table(:namespaces).create!(
- id: 1,
- name: 'GitLab Instance Administrators',
- path: 'gitlab-instance-administrators-random',
- type: 'Group'
- )
- end
-
- let(:self_monitoring_project) do
- table(:projects).create!(
- id: 2,
- name: 'Self Monitoring',
- path: 'self_monitoring',
- namespace_id: instance_administrators_group.id
- )
- end
-
- context 'when project ID is saved but group ID is not' do
- let(:application_settings) do
- application_settings_table.create!(instance_administration_project_id: self_monitoring_project.id)
- end
-
- it 'saves instance administrators group ID' do
- expect(application_settings.instance_administration_project_id).to eq(self_monitoring_project.id)
- expect(application_settings.instance_administrators_group_id).to be_nil
-
- migrate!
-
- expect(application_settings.reload.instance_administrators_group_id).to eq(instance_administrators_group.id)
- expect(application_settings.instance_administration_project_id).to eq(self_monitoring_project.id)
- end
- end
-
- context 'when group ID is saved but project ID is not' do
- let(:application_settings) do
- application_settings_table.create!(instance_administrators_group_id: instance_administrators_group.id)
- end
-
- it 'does not make changes' do
- expect(application_settings.instance_administrators_group_id).to eq(instance_administrators_group.id)
- expect(application_settings.instance_administration_project_id).to be_nil
-
- migrate!
-
- expect(application_settings.reload.instance_administrators_group_id).to eq(instance_administrators_group.id)
- expect(application_settings.instance_administration_project_id).to be_nil
- end
- end
-
- context 'when group ID and project ID are both saved' do
- let(:application_settings) do
- application_settings_table.create!(
- instance_administrators_group_id: instance_administrators_group.id,
- instance_administration_project_id: self_monitoring_project.id
- )
- end
-
- it 'does not make changes' do
- expect(application_settings.instance_administrators_group_id).to eq(instance_administrators_group.id)
- expect(application_settings.instance_administration_project_id).to eq(self_monitoring_project.id)
-
- migrate!
-
- expect(application_settings.reload.instance_administrators_group_id).to eq(instance_administrators_group.id)
- expect(application_settings.instance_administration_project_id).to eq(self_monitoring_project.id)
- end
- end
-
- context 'when neither group ID nor project ID is saved' do
- let(:application_settings) do
- application_settings_table.create!
- end
-
- it 'does not make changes' do
- expect(application_settings.instance_administrators_group_id).to be_nil
- expect(application_settings.instance_administration_project_id).to be_nil
-
- migrate!
-
- expect(application_settings.reload.instance_administrators_group_id).to be_nil
- expect(application_settings.instance_administration_project_id).to be_nil
- end
- end
-
- context 'when application_settings table has no rows' do
- it 'does not fail' do
- migrate!
- end
- end
-end
diff --git a/spec/migrations/schedule_add_primary_email_to_emails_if_user_confirmed_spec.rb b/spec/migrations/schedule_add_primary_email_to_emails_if_user_confirmed_spec.rb
new file mode 100644
index 00000000000..c66ac1bd7e9
--- /dev/null
+++ b/spec/migrations/schedule_add_primary_email_to_emails_if_user_confirmed_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe ScheduleAddPrimaryEmailToEmailsIfUserConfirmed, :sidekiq do
+ let(:migration) { described_class.new }
+ let(:users) { table(:users) }
+
+ let!(:user_1) { users.create!(name: 'confirmed-user-1', email: 'confirmed-1@example.com', confirmed_at: 1.day.ago, projects_limit: 100) }
+ let!(:user_2) { users.create!(name: 'confirmed-user-2', email: 'confirmed-2@example.com', confirmed_at: 1.day.ago, projects_limit: 100) }
+ let!(:user_3) { users.create!(name: 'confirmed-user-3', email: 'confirmed-3@example.com', confirmed_at: 1.day.ago, projects_limit: 100) }
+ let!(:user_4) { users.create!(name: 'confirmed-user-4', email: 'confirmed-4@example.com', confirmed_at: 1.day.ago, projects_limit: 100) }
+
+ before do
+ stub_const("#{described_class.name}::BATCH_SIZE", 2)
+ stub_const("#{described_class.name}::INTERVAL", 2.minutes.to_i)
+ end
+
+ it 'schedules addition of primary email to emails in delayed batches' do
+ Sidekiq::Testing.fake! do
+ freeze_time do
+ migration.up
+
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2.minutes, user_1.id, user_2.id)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, user_3.id, user_4.id)
+ expect(BackgroundMigrationWorker.jobs.size).to eq(2)
+ end
+ end
+ end
+end
diff --git a/spec/migrations/schedule_backfill_push_rules_id_in_projects_spec.rb b/spec/migrations/schedule_backfill_push_rules_id_in_projects_spec.rb
deleted file mode 100644
index 7b71110e62d..00000000000
--- a/spec/migrations/schedule_backfill_push_rules_id_in_projects_spec.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-RSpec.describe ScheduleBackfillPushRulesIdInProjects do
- let(:push_rules) { table(:push_rules) }
-
- it 'adds global rule association to application settings' do
- application_settings = table(:application_settings)
- setting = application_settings.create!
- sample_rule = push_rules.create!(is_sample: true)
-
- Sidekiq::Testing.fake! do
- disable_migrations_output { migrate! }
- end
-
- setting.reload
- expect(setting.push_rule_id).to eq(sample_rule.id)
- end
-
- it 'adds global rule association to last application settings when there is more than one record without failing' do
- application_settings = table(:application_settings)
- setting_old = application_settings.create!
- setting = application_settings.create!
- sample_rule = push_rules.create!(is_sample: true)
-
- Sidekiq::Testing.fake! do
- disable_migrations_output { migrate! }
- end
-
- expect(setting_old.reload.push_rule_id).to be_nil
- expect(setting.reload.push_rule_id).to eq(sample_rule.id)
- end
-
- it 'schedules worker to migrate project push rules' do
- rule_1 = push_rules.create!
- rule_2 = push_rules.create!
-
- Sidekiq::Testing.fake! do
- disable_migrations_output { migrate! }
-
- expect(BackgroundMigrationWorker.jobs.size).to eq(1)
- expect(described_class::MIGRATION)
- .to be_scheduled_delayed_migration(5.minutes, rule_1.id, rule_2.id)
- end
- end
-end
diff --git a/spec/migrations/schedule_blocked_by_links_replacement_second_try_spec.rb b/spec/migrations/schedule_blocked_by_links_replacement_second_try_spec.rb
deleted file mode 100644
index f2a0bdba32a..00000000000
--- a/spec/migrations/schedule_blocked_by_links_replacement_second_try_spec.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe ScheduleBlockedByLinksReplacementSecondTry do
- let(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab-org') }
- let(:project) { table(:projects).create!(namespace_id: namespace.id, name: 'gitlab') }
- let(:issue1) { table(:issues).create!(project_id: project.id, title: 'a') }
- let(:issue2) { table(:issues).create!(project_id: project.id, title: 'b') }
- let(:issue3) { table(:issues).create!(project_id: project.id, title: 'c') }
- let!(:issue_links) do
- [
- table(:issue_links).create!(source_id: issue1.id, target_id: issue2.id, link_type: 1),
- table(:issue_links).create!(source_id: issue2.id, target_id: issue1.id, link_type: 2),
- table(:issue_links).create!(source_id: issue1.id, target_id: issue3.id, link_type: 2)
- ]
- end
-
- before do
- stub_const("#{described_class.name}::BATCH_SIZE", 1)
- end
-
- it 'schedules jobs for blocked_by links' do
- Sidekiq::Testing.fake! do
- freeze_time do
- migrate!
-
- expect(described_class::MIGRATION).to be_scheduled_delayed_migration(
- 2.minutes, issue_links[1].id, issue_links[1].id)
- expect(described_class::MIGRATION).to be_scheduled_delayed_migration(
- 4.minutes, issue_links[2].id, issue_links[2].id)
- expect(BackgroundMigrationWorker.jobs.size).to eq(2)
- end
- end
- end
-end
diff --git a/spec/migrations/schedule_link_lfs_objects_projects_spec.rb b/spec/migrations/schedule_link_lfs_objects_projects_spec.rb
deleted file mode 100644
index 29c203c2c31..00000000000
--- a/spec/migrations/schedule_link_lfs_objects_projects_spec.rb
+++ /dev/null
@@ -1,76 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe ScheduleLinkLfsObjectsProjects, :migration, :sidekiq do
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:fork_networks) { table(:fork_networks) }
- let(:fork_network_members) { table(:fork_network_members) }
- let(:lfs_objects) { table(:lfs_objects) }
- let(:lfs_objects_projects) { table(:lfs_objects_projects) }
-
- let(:namespace) { namespaces.create!(name: 'GitLab', path: 'gitlab') }
-
- let(:fork_network) { fork_networks.create!(root_project_id: source_project.id) }
- let(:another_fork_network) { fork_networks.create!(root_project_id: another_source_project.id) }
-
- let(:source_project) { projects.create!(namespace_id: namespace.id) }
- let(:another_source_project) { projects.create!(namespace_id: namespace.id) }
- let(:project) { projects.create!(namespace_id: namespace.id) }
- let(:another_project) { projects.create!(namespace_id: namespace.id) }
-
- let(:lfs_object) { lfs_objects.create!(oid: 'abc123', size: 100) }
- let(:another_lfs_object) { lfs_objects.create!(oid: 'def456', size: 200) }
-
- let!(:source_project_lop_1) do
- lfs_objects_projects.create!(
- lfs_object_id: lfs_object.id,
- project_id: source_project.id
- )
- end
-
- let!(:source_project_lop_2) do
- lfs_objects_projects.create!(
- lfs_object_id: another_lfs_object.id,
- project_id: source_project.id
- )
- end
-
- let!(:another_source_project_lop_1) do
- lfs_objects_projects.create!(
- lfs_object_id: lfs_object.id,
- project_id: another_source_project.id
- )
- end
-
- let!(:another_source_project_lop_2) do
- lfs_objects_projects.create!(
- lfs_object_id: another_lfs_object.id,
- project_id: another_source_project.id
- )
- end
-
- before do
- stub_const("#{described_class.name}::BATCH_SIZE", 2)
-
- # Create links between projects
- fork_network_members.create!(fork_network_id: fork_network.id, project_id: source_project.id, forked_from_project_id: nil)
- fork_network_members.create!(fork_network_id: fork_network.id, project_id: project.id, forked_from_project_id: source_project.id)
- fork_network_members.create!(fork_network_id: another_fork_network.id, project_id: another_source_project.id, forked_from_project_id: nil)
- fork_network_members.create!(fork_network_id: another_fork_network.id, project_id: another_project.id, forked_from_project_id: another_fork_network.root_project_id)
- end
-
- it 'schedules background migration to link LFS objects' do
- Sidekiq::Testing.fake! do
- migrate!
-
- expect(BackgroundMigrationWorker.jobs.size).to eq(2)
- expect(described_class::MIGRATION)
- .to be_scheduled_delayed_migration(2.minutes, source_project_lop_1.id, source_project_lop_2.id)
- expect(described_class::MIGRATION)
- .to be_scheduled_delayed_migration(4.minutes, another_source_project_lop_1.id, another_source_project_lop_2.id)
- end
- end
-end
diff --git a/spec/migrations/schedule_merge_request_cleanup_schedules_backfill_spec.rb b/spec/migrations/schedule_merge_request_cleanup_schedules_backfill_spec.rb
deleted file mode 100644
index 319c0802f2c..00000000000
--- a/spec/migrations/schedule_merge_request_cleanup_schedules_backfill_spec.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-RSpec.describe ScheduleMergeRequestCleanupSchedulesBackfill, :sidekiq, schema: 20201023114628 do
- let(:merge_requests) { table(:merge_requests) }
- let(:cleanup_schedules) { table(:merge_request_cleanup_schedules) }
-
- let(:namespace) { table(:namespaces).create!(name: 'name', path: 'path') }
- let(:project) { table(:projects).create!(namespace_id: namespace.id) }
-
- describe '#up' do
- let!(:open_mr) { merge_requests.create!(target_project_id: project.id, source_branch: 'master', target_branch: 'master') }
-
- let!(:closed_mr_1) { merge_requests.create!(target_project_id: project.id, source_branch: 'master', target_branch: 'master', state_id: 2) }
- let!(:closed_mr_2) { merge_requests.create!(target_project_id: project.id, source_branch: 'master', target_branch: 'master', state_id: 2) }
-
- let!(:merged_mr_1) { merge_requests.create!(target_project_id: project.id, source_branch: 'master', target_branch: 'master', state_id: 3) }
- let!(:merged_mr_2) { merge_requests.create!(target_project_id: project.id, source_branch: 'master', target_branch: 'master', state_id: 3) }
-
- before do
- stub_const("#{described_class}::BATCH_SIZE", 2)
- end
-
- it 'schedules BackfillMergeRequestCleanupSchedules background jobs' do
- Sidekiq::Testing.fake! do
- migrate!
-
- aggregate_failures do
- expect(described_class::MIGRATION)
- .to be_scheduled_delayed_migration(2.minutes, closed_mr_1.id, closed_mr_2.id)
- expect(described_class::MIGRATION)
- .to be_scheduled_delayed_migration(4.minutes, merged_mr_1.id, merged_mr_2.id)
- expect(BackgroundMigrationWorker.jobs.size).to eq(2)
- end
- end
- end
- end
-end
diff --git a/spec/migrations/schedule_migrate_security_scans_spec.rb b/spec/migrations/schedule_migrate_security_scans_spec.rb
deleted file mode 100644
index ce926241ba6..00000000000
--- a/spec/migrations/schedule_migrate_security_scans_spec.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe ScheduleMigrateSecurityScans, :sidekiq do
- let(:migration) { described_class.new }
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:builds) { table(:ci_builds) }
- let(:job_artifacts) { table(:ci_job_artifacts) }
-
- let(:namespace) { namespaces.create!(name: "foo", path: "bar") }
- let(:project) { projects.create!(namespace_id: namespace.id) }
- let(:job) { builds.create! }
-
- before do
- stub_const("#{described_class.name}::BATCH_SIZE", 1)
- stub_const("#{described_class.name}::INTERVAL", 5.minutes.to_i)
- end
-
- context 'no security job artifacts' do
- before do
- table(:ci_job_artifacts)
- end
-
- it 'does not schedule migration' do
- Sidekiq::Testing.fake! do
- migrate!
-
- expect(BackgroundMigrationWorker.jobs).to be_empty
- end
- end
- end
-
- context 'has security job artifacts' do
- 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
- freeze_time do
- migration.up
-
- expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, job_artifact_1.id, job_artifact_1.id)
- expect(described_class::MIGRATION).to be_scheduled_delayed_migration(10.minutes, job_artifact_2.id, job_artifact_2.id)
- expect(BackgroundMigrationWorker.jobs.size).to eq(2)
- end
- end
- end
- end
-
- context 'has non-security job artifacts' do
- 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
- freeze_time do
- migration.up
-
- expect(BackgroundMigrationWorker.jobs).to be_empty
- end
- end
- end
- end
-end
diff --git a/spec/migrations/schedule_migrate_u2f_webauthn_spec.rb b/spec/migrations/schedule_migrate_u2f_webauthn_spec.rb
deleted file mode 100644
index 48f098e34fc..00000000000
--- a/spec/migrations/schedule_migrate_u2f_webauthn_spec.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe ScheduleMigrateU2fWebauthn do
- let(:migration_name) { described_class::MIGRATION }
- let(:u2f_registrations) { table(:u2f_registrations) }
- let(:webauthn_registrations) { table(:webauthn_registrations) }
-
- let(:users) { table(:users) }
-
- let(:user) { users.create!(email: 'email@email.com', name: 'foo', username: 'foo', projects_limit: 0) }
-
- before do
- stub_const("#{described_class.name}::BATCH_SIZE", 1)
- end
-
- context 'when there are u2f registrations' do
- let!(:u2f_reg_1) { create_u2f_registration(1, 'reg1') }
- let!(:u2f_reg_2) { create_u2f_registration(2, 'reg2') }
-
- it 'schedules a background migration' do
- Sidekiq::Testing.fake! do
- freeze_time do
- migrate!
-
- expect(migration_name).to be_scheduled_delayed_migration(2.minutes, 1, 1)
- expect(migration_name).to be_scheduled_delayed_migration(4.minutes, 2, 2)
- expect(BackgroundMigrationWorker.jobs.size).to eq(2)
- end
- end
- end
- end
-
- context 'when there are no u2f registrations' do
- it 'does not schedule background migrations' do
- Sidekiq::Testing.fake! do
- freeze_time do
- migrate!
-
- expect(BackgroundMigrationWorker.jobs.size).to eq(0)
- end
- end
- end
- end
-
- def create_u2f_registration(id, name)
- device = U2F::FakeU2F.new(FFaker::BaconIpsum.characters(5))
- u2f_registrations.create!({ id: id,
- 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),
- counter: 5,
- name: name,
- user_id: user.id })
- end
-end
diff --git a/spec/migrations/schedule_populate_has_vulnerabilities_spec.rb b/spec/migrations/schedule_populate_has_vulnerabilities_spec.rb
deleted file mode 100644
index edae7330b1e..00000000000
--- a/spec/migrations/schedule_populate_has_vulnerabilities_spec.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe SchedulePopulateHasVulnerabilities do
- let(:users) { table(:users) }
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:vulnerabilities) { table(:vulnerabilities) }
- let(:user) { users.create!(name: 'test', email: 'test@example.com', projects_limit: 5) }
- let(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') }
- let(:vulnerability_base_params) { { title: 'title', state: 2, severity: 0, confidence: 5, report_type: 2, author_id: user.id } }
- let!(:project_1) { projects.create!(namespace_id: namespace.id, name: 'foo_1') }
- let!(:project_2) { projects.create!(namespace_id: namespace.id, name: 'foo_2') }
- let!(:project_3) { projects.create!(namespace_id: namespace.id, name: 'foo_3') }
-
- around do |example|
- freeze_time { Sidekiq::Testing.fake! { example.run } }
- end
-
- before do
- stub_const("#{described_class.name}::BATCH_SIZE", 1)
-
- vulnerabilities.create!(vulnerability_base_params.merge(project_id: project_1.id))
- vulnerabilities.create!(vulnerability_base_params.merge(project_id: project_3.id))
- end
-
- it 'schedules the background jobs', :aggregate_failures do
- migrate!
-
- expect(BackgroundMigrationWorker.jobs.size).to be(2)
- expect(described_class::MIGRATION_CLASS).to be_scheduled_delayed_migration(2.minutes, project_1.id)
- expect(described_class::MIGRATION_CLASS).to be_scheduled_delayed_migration(4.minutes, project_3.id)
- end
-end
diff --git a/spec/migrations/schedule_populate_issue_email_participants_spec.rb b/spec/migrations/schedule_populate_issue_email_participants_spec.rb
deleted file mode 100644
index 3a7a4e4df1e..00000000000
--- a/spec/migrations/schedule_populate_issue_email_participants_spec.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-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/migrations/schedule_populate_missing_dismissal_information_for_vulnerabilities_spec.rb b/spec/migrations/schedule_populate_missing_dismissal_information_for_vulnerabilities_spec.rb
deleted file mode 100644
index e5934f2171f..00000000000
--- a/spec/migrations/schedule_populate_missing_dismissal_information_for_vulnerabilities_spec.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe SchedulePopulateMissingDismissalInformationForVulnerabilities do
- let(:users) { table(:users) }
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:vulnerabilities) { table(:vulnerabilities) }
- let(:user) { users.create!(name: 'test', email: 'test@example.com', projects_limit: 5) }
- let(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') }
- let(:project) { projects.create!(namespace_id: namespace.id, name: 'foo') }
-
- let!(:vulnerability_1) { vulnerabilities.create!(title: 'title', state: 2, severity: 0, confidence: 5, report_type: 2, project_id: project.id, author_id: user.id) }
- let!(:vulnerability_2) { vulnerabilities.create!(title: 'title', state: 2, severity: 0, confidence: 5, report_type: 2, project_id: project.id, author_id: user.id, dismissed_at: Time.now) }
- let!(:vulnerability_3) { vulnerabilities.create!(title: 'title', state: 2, severity: 0, confidence: 5, report_type: 2, project_id: project.id, author_id: user.id, dismissed_by_id: user.id) }
- let!(:vulnerability_4) { vulnerabilities.create!(title: 'title', state: 2, severity: 0, confidence: 5, report_type: 2, project_id: project.id, author_id: user.id, dismissed_at: Time.now, dismissed_by_id: user.id) }
- let!(:vulnerability_5) { vulnerabilities.create!(title: 'title', state: 1, severity: 0, confidence: 5, report_type: 2, project_id: project.id, author_id: user.id) }
-
- around do |example|
- freeze_time { Sidekiq::Testing.fake! { example.run } }
- end
-
- before do
- stub_const("#{described_class.name}::BATCH_SIZE", 1)
- end
-
- it 'schedules the background jobs', :aggregate_failures do
- migrate!
-
- expect(BackgroundMigrationWorker.jobs.size).to be(3)
- expect(described_class::MIGRATION_CLASS).to be_scheduled_delayed_migration(3.minutes, vulnerability_1.id)
- expect(described_class::MIGRATION_CLASS).to be_scheduled_delayed_migration(6.minutes, vulnerability_2.id)
- expect(described_class::MIGRATION_CLASS).to be_scheduled_delayed_migration(9.minutes, vulnerability_3.id)
- end
-end
diff --git a/spec/migrations/schedule_populate_personal_snippet_statistics_spec.rb b/spec/migrations/schedule_populate_personal_snippet_statistics_spec.rb
deleted file mode 100644
index 5f764a1ee8f..00000000000
--- a/spec/migrations/schedule_populate_personal_snippet_statistics_spec.rb
+++ /dev/null
@@ -1,60 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-RSpec.describe SchedulePopulatePersonalSnippetStatistics do
- let(:users) { table(:users) }
- let(:namespaces) { table(:namespaces) }
- let(:snippets) { table(:snippets) }
- let(:projects) { table(:projects) }
- let!(:user1) { users.create!(id: 1, email: 'user1@example.com', projects_limit: 10, username: 'test1', name: 'Test1', state: 'active') }
- let!(:user2) { users.create!(id: 2, email: 'user2@example.com', projects_limit: 10, username: 'test2', name: 'Test2', state: 'active') }
- let!(:user3) { users.create!(id: 3, email: 'user3@example.com', projects_limit: 10, username: 'test3', name: 'Test3', state: 'active') }
- let!(:namespace1) { namespaces.create!(id: 1, owner_id: user1.id, name: 'test1', path: 'test1') }
- let!(:namespace2) { namespaces.create!(id: 2, owner_id: user2.id, name: 'test2', path: 'test2') }
- let!(:namespace3) { namespaces.create!(id: 3, owner_id: user3.id, name: 'test3', path: 'test3') }
-
- def create_snippet(id, user_id, type = 'PersonalSnippet')
- params = {
- id: id,
- type: type,
- author_id: user_id,
- file_name: 'foo',
- content: 'bar'
- }
-
- snippets.create!(params)
- end
-
- it 'correctly schedules background migrations' do
- # Creating the snippets in different order
- create_snippet(1, user1.id)
- create_snippet(2, user2.id)
- create_snippet(3, user1.id)
- create_snippet(4, user3.id)
- create_snippet(5, user3.id)
- create_snippet(6, user1.id)
- # Creating a project snippet to ensure we don't pick it
- create_snippet(7, user1.id, 'ProjectSnippet')
-
- stub_const("#{described_class}::BATCH_SIZE", 4)
-
- Sidekiq::Testing.fake! do
- freeze_time do
- migrate!
-
- aggregate_failures do
- expect(described_class::MIGRATION)
- .to be_scheduled_migration([1, 3, 6, 2])
-
- expect(described_class::MIGRATION)
- .to be_scheduled_delayed_migration(2.minutes, [4, 5])
-
- expect(BackgroundMigrationWorker.jobs.size).to eq(2)
- end
- end
- end
- end
-end
diff --git a/spec/migrations/schedule_populate_project_snippet_statistics_spec.rb b/spec/migrations/schedule_populate_project_snippet_statistics_spec.rb
deleted file mode 100644
index 4ac107c5202..00000000000
--- a/spec/migrations/schedule_populate_project_snippet_statistics_spec.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe SchedulePopulateProjectSnippetStatistics do
- let(:users) { table(:users) }
- let(:snippets) { table(:snippets) }
- let(:projects) { table(:projects) }
- let(:namespaces) { table(:namespaces) }
- let(:user1) { users.create!(id: 1, email: 'user1@example.com', projects_limit: 10, username: 'test1', name: 'Test1', state: 'active') }
- let(:user2) { users.create!(id: 2, email: 'user2@example.com', projects_limit: 10, username: 'test2', name: 'Test2', state: 'active') }
- let(:namespace1) { namespaces.create!(id: 1, owner_id: user1.id, name: 'user1', path: 'user1') }
- let(:namespace2) { namespaces.create!(id: 2, owner_id: user2.id, name: 'user2', path: 'user2') }
- let(:project1) { projects.create!(id: 1, namespace_id: namespace1.id) }
- let(:project2) { projects.create!(id: 2, namespace_id: namespace1.id) }
- let(:project3) { projects.create!(id: 3, namespace_id: namespace2.id) }
-
- def create_snippet(id, user_id, project_id, type = 'ProjectSnippet')
- params = {
- id: id,
- type: type,
- author_id: user_id,
- project_id: project_id,
- file_name: 'foo',
- content: 'bar'
- }
-
- snippets.create!(params)
- end
-
- it 'correctly schedules background migrations' do
- # Creating the snippets in different order
- create_snippet(1, user1.id, project1.id)
- create_snippet(2, user2.id, project3.id)
- create_snippet(3, user1.id, project1.id)
- create_snippet(4, user1.id, project2.id)
- create_snippet(5, user2.id, project3.id)
- create_snippet(6, user1.id, project1.id)
- # Creating a personal snippet to ensure we don't pick it
- create_snippet(7, user1.id, nil, 'PersonalSnippet')
-
- stub_const("#{described_class}::BATCH_SIZE", 4)
-
- Sidekiq::Testing.fake! do
- freeze_time do
- migrate!
-
- aggregate_failures do
- expect(described_class::MIGRATION)
- .to be_scheduled_migration([1, 3, 6, 4])
-
- expect(described_class::MIGRATION)
- .to be_scheduled_delayed_migration(2.minutes, [2, 5])
-
- expect(BackgroundMigrationWorker.jobs.size).to eq(2)
- end
- end
- end
- end
-end
diff --git a/spec/migrations/schedule_populate_user_highest_roles_table_spec.rb b/spec/migrations/schedule_populate_user_highest_roles_table_spec.rb
deleted file mode 100644
index 0a2ee82b349..00000000000
--- a/spec/migrations/schedule_populate_user_highest_roles_table_spec.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe SchedulePopulateUserHighestRolesTable do
- let(:users) { table(:users) }
-
- def create_user(id, params = {})
- user_params = {
- id: id,
- state: 'active',
- user_type: nil,
- bot_type: nil,
- ghost: nil,
- email: "user#{id}@example.com",
- projects_limit: 0
- }.merge(params)
-
- users.create!(user_params)
- end
-
- it 'correctly schedules background migrations' do
- create_user(1)
- create_user(2, state: 'blocked')
- create_user(3, user_type: 2)
- create_user(4)
- create_user(5, bot_type: 1)
- create_user(6, ghost: true)
- create_user(7, ghost: false)
-
- 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(5.minutes, 1, 4)
-
- expect(described_class::MIGRATION).to be_scheduled_delayed_migration(10.minutes, 7, 7)
-
- expect(BackgroundMigrationWorker.jobs.size).to eq(2)
- end
- end
- end
-end
diff --git a/spec/migrations/schedule_recalculate_project_authorizations_second_run_spec.rb b/spec/migrations/schedule_recalculate_project_authorizations_second_run_spec.rb
deleted file mode 100644
index 380d107250b..00000000000
--- a/spec/migrations/schedule_recalculate_project_authorizations_second_run_spec.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe ScheduleRecalculateProjectAuthorizationsSecondRun do
- let(:users_table) { table(:users) }
-
- before do
- stub_const("#{described_class}::BATCH_SIZE", 2)
-
- 1.upto(4) do |i|
- users_table.create!(id: i, name: "user#{i}", email: "user#{i}@example.com", projects_limit: 1)
- end
- end
-
- it 'schedules background migration' do
- Sidekiq::Testing.fake! do
- freeze_time do
- migrate!
-
- expect(BackgroundMigrationWorker.jobs.size).to eq(2)
- expect(described_class::MIGRATION).to be_scheduled_migration(1, 2)
- expect(described_class::MIGRATION).to be_scheduled_migration(3, 4)
- end
- end
- end
-end
diff --git a/spec/migrations/schedule_recalculate_project_authorizations_spec.rb b/spec/migrations/schedule_recalculate_project_authorizations_spec.rb
deleted file mode 100644
index a4400c2ac83..00000000000
--- a/spec/migrations/schedule_recalculate_project_authorizations_spec.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe ScheduleRecalculateProjectAuthorizations do
- let(:users_table) { table(:users) }
- let(:namespaces_table) { table(:namespaces) }
- let(:projects_table) { table(:projects) }
- let(:project_authorizations_table) { table(:project_authorizations) }
-
- let(:user1) { users_table.create!(name: 'user1', email: 'user1@example.com', projects_limit: 1) }
- let(:user2) { users_table.create!(name: 'user2', email: 'user2@example.com', projects_limit: 1) }
- let(:group) { namespaces_table.create!(id: 1, type: 'Group', name: 'group', path: 'group') }
- let(:project) do
- projects_table.create!(id: 1, name: 'project', path: 'project',
- visibility_level: 0, namespace_id: group.id)
- end
-
- before do
- stub_const("#{described_class}::BATCH_SIZE", 1)
-
- project_authorizations_table.create!(user_id: user1.id, project_id: project.id, access_level: 30)
- project_authorizations_table.create!(user_id: user2.id, project_id: project.id, access_level: 30)
- end
-
- it 'schedules background migration' do
- Sidekiq::Testing.fake! do
- freeze_time do
- migrate!
-
- expect(BackgroundMigrationWorker.jobs.size).to eq(2)
- expect(described_class::MIGRATION).to be_scheduled_migration([user1.id])
- expect(described_class::MIGRATION).to be_scheduled_migration([user2.id])
- end
- end
- end
-
- it 'ignores projects with higher id than maximum group id' do
- another_user = users_table.create!(name: 'another user', email: 'another-user@example.com',
- projects_limit: 1)
- ignored_project = projects_table.create!(id: 2, name: 'ignored-project', path: 'ignored-project',
- visibility_level: 0, namespace_id: group.id)
- project_authorizations_table.create!(user_id: another_user.id, project_id: ignored_project.id,
- access_level: 30)
-
- Sidekiq::Testing.fake! do
- freeze_time do
- migrate!
-
- expect(BackgroundMigrationWorker.jobs.size).to eq(2)
- expect(described_class::MIGRATION).to be_scheduled_migration([user1.id])
- expect(described_class::MIGRATION).to be_scheduled_migration([user2.id])
- end
- end
- end
-end
diff --git a/spec/migrations/schedule_recalculate_project_authorizations_third_run_spec.rb b/spec/migrations/schedule_recalculate_project_authorizations_third_run_spec.rb
deleted file mode 100644
index 302ae1d5ebe..00000000000
--- a/spec/migrations/schedule_recalculate_project_authorizations_third_run_spec.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe ScheduleRecalculateProjectAuthorizationsThirdRun do
- let(:users_table) { table(:users) }
-
- before do
- stub_const("#{described_class}::BATCH_SIZE", 2)
-
- 1.upto(4) do |i|
- users_table.create!(id: i, name: "user#{i}", email: "user#{i}@example.com", projects_limit: 1)
- end
- end
-
- it 'schedules background migration' do
- Sidekiq::Testing.fake! do
- freeze_time do
- migrate!
-
- expect(BackgroundMigrationWorker.jobs.size).to eq(2)
- expect(described_class::MIGRATION).to be_scheduled_migration(1, 2)
- expect(described_class::MIGRATION).to be_scheduled_migration(3, 4)
- end
- end
- end
-end
diff --git a/spec/migrations/schedule_repopulate_historical_vulnerability_statistics_spec.rb b/spec/migrations/schedule_repopulate_historical_vulnerability_statistics_spec.rb
deleted file mode 100644
index a65c94cf60e..00000000000
--- a/spec/migrations/schedule_repopulate_historical_vulnerability_statistics_spec.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe ScheduleRepopulateHistoricalVulnerabilityStatistics do
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:project_settings) { table(:project_settings) }
-
- let(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') }
- let!(:project_1) { projects.create!(namespace_id: namespace.id, name: 'foo_1') }
- let!(:project_2) { projects.create!(namespace_id: namespace.id, name: 'foo_2') }
- let!(:project_3) { projects.create!(namespace_id: namespace.id, name: 'foo_3') }
- let!(:project_4) { projects.create!(namespace_id: namespace.id, name: 'foo_4') }
-
- around do |example|
- freeze_time { Sidekiq::Testing.fake! { example.run } }
- end
-
- before do
- stub_const("#{described_class.name}::BATCH_SIZE", 1)
-
- project_settings.create!(project_id: project_1.id, has_vulnerabilities: true)
- project_settings.create!(project_id: project_2.id, has_vulnerabilities: false)
- project_settings.create!(project_id: project_4.id, has_vulnerabilities: true)
- end
-
- it 'schedules the background jobs', :aggregate_failures do
- migrate!
-
- expect(BackgroundMigrationWorker.jobs.size).to be(2)
- expect(described_class::MIGRATION_CLASS).to be_scheduled_delayed_migration(described_class::DELAY_INTERVAL, [project_1.id], described_class::DAY_COUNT)
- expect(described_class::MIGRATION_CLASS).to be_scheduled_delayed_migration(2 * described_class::DELAY_INTERVAL, [project_4.id], described_class::DAY_COUNT)
- end
-end
diff --git a/spec/migrations/schedule_update_existing_subgroup_to_match_visibility_level_of_parent_spec.rb b/spec/migrations/schedule_update_existing_subgroup_to_match_visibility_level_of_parent_spec.rb
deleted file mode 100644
index 8f265acccae..00000000000
--- a/spec/migrations/schedule_update_existing_subgroup_to_match_visibility_level_of_parent_spec.rb
+++ /dev/null
@@ -1,79 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe ScheduleUpdateExistingSubgroupToMatchVisibilityLevelOfParent do
- include MigrationHelpers::NamespacesHelpers
- let(:migration_class) { described_class::MIGRATION }
- let(:migration_name) { migration_class.to_s.demodulize }
-
- context 'private visibility level' do
- it 'correctly schedules background migrations' do
- parent = create_namespace('parent', Gitlab::VisibilityLevel::PRIVATE)
- create_namespace('child', Gitlab::VisibilityLevel::PUBLIC, parent_id: parent.id)
-
- Sidekiq::Testing.fake! do
- freeze_time do
- migrate!
-
- expect(BackgroundMigrationWorker.jobs.size).to eq(1)
- expect(migration_name).to be_scheduled_migration_with_multiple_args([parent.id], Gitlab::VisibilityLevel::PRIVATE)
- end
- end
- end
-
- it 'correctly schedules background migrations for groups and subgroups' do
- parent = create_namespace('parent', Gitlab::VisibilityLevel::PRIVATE)
- middle_group = create_namespace('middle_group', Gitlab::VisibilityLevel::PRIVATE, parent_id: parent.id)
- create_namespace('middle_empty_group', Gitlab::VisibilityLevel::PRIVATE, parent_id: parent.id)
- create_namespace('child', Gitlab::VisibilityLevel::PUBLIC, parent_id: middle_group.id)
-
- Sidekiq::Testing.fake! do
- freeze_time do
- migrate!
-
- expect(BackgroundMigrationWorker.jobs.size).to eq(1)
- expect(migration_name).to be_scheduled_migration_with_multiple_args([middle_group.id, parent.id], Gitlab::VisibilityLevel::PRIVATE)
- end
- end
- end
- end
-
- context 'internal visibility level' do
- it 'correctly schedules background migrations' do
- parent = create_namespace('parent', Gitlab::VisibilityLevel::INTERNAL)
- middle_group = create_namespace('child', Gitlab::VisibilityLevel::INTERNAL, parent_id: parent.id)
- create_namespace('child', Gitlab::VisibilityLevel::PUBLIC, parent_id: middle_group.id)
-
- Sidekiq::Testing.fake! do
- freeze_time do
- migrate!
-
- expect(BackgroundMigrationWorker.jobs.size).to eq(1)
- expect(migration_name).to be_scheduled_migration_with_multiple_args([parent.id, middle_group.id], Gitlab::VisibilityLevel::INTERNAL)
- end
- end
- end
- end
-
- context 'mixed visibility levels' do
- it 'correctly schedules background migrations' do
- parent1 = create_namespace('parent1', Gitlab::VisibilityLevel::INTERNAL)
- create_namespace('child', Gitlab::VisibilityLevel::PUBLIC, parent_id: parent1.id)
- parent2 = create_namespace('parent2', Gitlab::VisibilityLevel::PRIVATE)
- middle_group = create_namespace('middle_group', Gitlab::VisibilityLevel::INTERNAL, parent_id: parent2.id)
- create_namespace('child', Gitlab::VisibilityLevel::PUBLIC, parent_id: middle_group.id)
-
- Sidekiq::Testing.fake! do
- freeze_time do
- migrate!
-
- expect(BackgroundMigrationWorker.jobs.size).to eq(2)
- expect(migration_name).to be_scheduled_migration_with_multiple_args([parent1.id, middle_group.id], Gitlab::VisibilityLevel::INTERNAL)
- expect(migration_name).to be_scheduled_migration_with_multiple_args([parent2.id], Gitlab::VisibilityLevel::PRIVATE)
- end
- end
- end
- end
-end
diff --git a/spec/migrations/schedule_update_existing_users_that_require_two_factor_auth_spec.rb b/spec/migrations/schedule_update_existing_users_that_require_two_factor_auth_spec.rb
deleted file mode 100644
index a839229ec22..00000000000
--- a/spec/migrations/schedule_update_existing_users_that_require_two_factor_auth_spec.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe ScheduleUpdateExistingUsersThatRequireTwoFactorAuth do
- let(:users) { table(:users) }
- let!(:user_1) { users.create!(require_two_factor_authentication_from_group: true, name: "user1", email: "user1@example.com", projects_limit: 1) }
- let!(:user_2) { users.create!(require_two_factor_authentication_from_group: false, name: "user2", email: "user2@example.com", projects_limit: 1) }
- let!(:user_3) { users.create!(require_two_factor_authentication_from_group: true, name: "user3", email: "user3@example.com", projects_limit: 1) }
-
- before do
- stub_const("#{described_class.name}::BATCH_SIZE", 1)
- end
-
- it 'schedules jobs for users that require two factor authentication' do
- Sidekiq::Testing.fake! do
- freeze_time do
- migrate!
-
- expect(described_class::MIGRATION).to be_scheduled_delayed_migration(
- 2.minutes, user_1.id, user_1.id)
- expect(described_class::MIGRATION).to be_scheduled_delayed_migration(
- 4.minutes, user_3.id, user_3.id)
- expect(BackgroundMigrationWorker.jobs.size).to eq(2)
- end
- end
- end
-end
diff --git a/spec/migrations/seed_merge_trains_enabled_spec.rb b/spec/migrations/seed_merge_trains_enabled_spec.rb
deleted file mode 100644
index 1cb0e3cf8a6..00000000000
--- a/spec/migrations/seed_merge_trains_enabled_spec.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe SeedMergeTrainsEnabled do
- describe 'migrate' do
- let(:project_ci_cd_settings) { table(:project_ci_cd_settings) }
- let(:projects) { table(:projects) }
- let(:namespaces) { table(:namespaces) }
-
- context 'when on Gitlab.com' do
- before do
- namespace = namespaces.create!(name: 'hello', path: 'hello/')
- project1 = projects.create!(namespace_id: namespace.id)
- project2 = projects.create!(namespace_id: namespace.id)
- project_ci_cd_settings.create!(project_id: project1.id, merge_pipelines_enabled: true)
- project_ci_cd_settings.create!(project_id: project2.id, merge_pipelines_enabled: false)
- end
-
- it 'updates merge_trains_enabled to true for where merge_pipelines_enabled is true' do
- migrate!
-
- expect(project_ci_cd_settings.where(merge_trains_enabled: true).count).to be(1)
- end
- end
- end
-end
diff --git a/spec/migrations/seed_repository_storages_weighted_spec.rb b/spec/migrations/seed_repository_storages_weighted_spec.rb
deleted file mode 100644
index 102107bcc9f..00000000000
--- a/spec/migrations/seed_repository_storages_weighted_spec.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe SeedRepositoryStoragesWeighted do
- let(:storages) { { "foo" => {}, "baz" => {} } }
- let(:application_settings) do
- table(:application_settings).tap do |klass|
- klass.class_eval do
- serialize :repository_storages
- end
- end
- end
-
- before do
- allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
- end
-
- let(:application_setting) { application_settings.create! }
- let(:repository_storages) { ["foo"] }
-
- it 'correctly schedules background migrations' do
- application_setting.repository_storages = repository_storages
- application_setting.save!
-
- migrate!
-
- expect(application_settings.find(application_setting.id).repository_storages_weighted).to eq({ "foo" => 100, "baz" => 0 })
- end
-end
diff --git a/spec/migrations/services_remove_temporary_index_on_project_id_spec.rb b/spec/migrations/services_remove_temporary_index_on_project_id_spec.rb
deleted file mode 100644
index d47f6deb2d5..00000000000
--- a/spec/migrations/services_remove_temporary_index_on_project_id_spec.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe ServicesRemoveTemporaryIndexOnProjectId do
- let(:migration_instance) { described_class.new }
-
- it 'adds and removes temporary partial index in up and down methods' do
- reversible_migration do |migration|
- migration.before -> {
- expect(migration_instance.index_exists?(:services, :project_id, name: described_class::INDEX_NAME)).to be true
- }
-
- migration.after -> {
- expect(migration_instance.index_exists?(:services, :project_id, name: described_class::INDEX_NAME)).to be false
- }
- end
- end
-
- describe '#up' do
- context 'index does not exist' do
- it 'skips removal action' do
- migrate!
-
- expect { migrate! }.not_to change { migration_instance.index_exists?(:services, :project_id, name: described_class::INDEX_NAME) }
- end
- end
- end
-
- describe '#down' do
- context 'index already exists' do
- it 'skips creation of duplicated temporary partial index on project_id' do
- schema_migrate_down!
-
- expect { schema_migrate_down! }.not_to change { migration_instance.index_exists?(:services, :project_id, name: described_class::INDEX_NAME) }
- end
- end
- end
-end
diff --git a/spec/migrations/set_job_waiter_ttl_spec.rb b/spec/migrations/set_job_waiter_ttl_spec.rb
deleted file mode 100644
index a051f8a535c..00000000000
--- a/spec/migrations/set_job_waiter_ttl_spec.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe SetJobWaiterTtl, :redis do
- it 'sets TTLs where necessary' do
- waiter_with_ttl = Gitlab::JobWaiter.new.key
- waiter_without_ttl = Gitlab::JobWaiter.new.key
- key_with_ttl = "foo:bar"
- key_without_ttl = "foo:qux"
-
- Gitlab::Redis::SharedState.with do |redis|
- redis.set(waiter_with_ttl, "zzz", ex: 2000)
- redis.set(waiter_without_ttl, "zzz")
- redis.set(key_with_ttl, "zzz", ex: 2000)
- redis.set(key_without_ttl, "zzz")
-
- described_class.new.up
-
- # This is the point of the migration. We know the migration uses a TTL of 21_600
- expect(redis.ttl(waiter_without_ttl)).to be > 20_000
-
- # Other TTL's should be untouched by the migration
- expect(redis.ttl(waiter_with_ttl)).to be_between(1000, 2000)
- expect(redis.ttl(key_with_ttl)).to be_between(1000, 2000)
- expect(redis.ttl(key_without_ttl)).to eq(-1)
- end
- end
-end
diff --git a/spec/migrations/slice_merge_request_diff_commit_migrations_spec.rb b/spec/migrations/slice_merge_request_diff_commit_migrations_spec.rb
index 1fd19ee42b4..e03dd73ec8b 100644
--- a/spec/migrations/slice_merge_request_diff_commit_migrations_spec.rb
+++ b/spec/migrations/slice_merge_request_diff_commit_migrations_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration! 'slice_merge_request_diff_commit_migrations'
+require_migration!
RSpec.describe SliceMergeRequestDiffCommitMigrations, :migration do
let(:migration) { described_class.new }
diff --git a/spec/migrations/steal_merge_request_diff_commit_users_migration_spec.rb b/spec/migrations/steal_merge_request_diff_commit_users_migration_spec.rb
index 3ad0b5a93c2..4fb4ba61a34 100644
--- a/spec/migrations/steal_merge_request_diff_commit_users_migration_spec.rb
+++ b/spec/migrations/steal_merge_request_diff_commit_users_migration_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require_migration! 'steal_merge_request_diff_commit_users_migration'
+require_migration!
RSpec.describe StealMergeRequestDiffCommitUsersMigration, :migration do
let(:migration) { described_class.new }
diff --git a/spec/migrations/unconfirm_wrongfully_verified_emails_spec.rb b/spec/migrations/unconfirm_wrongfully_verified_emails_spec.rb
deleted file mode 100644
index 5adc866d0a5..00000000000
--- a/spec/migrations/unconfirm_wrongfully_verified_emails_spec.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe UnconfirmWrongfullyVerifiedEmails do
- before do
- user = table(:users).create!(name: 'user1', email: 'test1@test.com', projects_limit: 1)
- table(:emails).create!(email: 'test2@test.com', user_id: user.id)
- end
-
- context 'when email confirmation is enabled' do
- before do
- table(:application_settings).create!(send_user_confirmation_email: true)
- end
-
- it 'enqueues WrongullyConfirmedEmailUnconfirmer job' do
- Sidekiq::Testing.fake! do
- migrate!
-
- jobs = BackgroundMigrationWorker.jobs
- expect(jobs.size).to eq(1)
- expect(jobs.first["args"].first).to eq(Gitlab::BackgroundMigration::WrongfullyConfirmedEmailUnconfirmer.name.demodulize)
- end
- end
- end
-
- context 'when email confirmation is disabled' do
- before do
- table(:application_settings).create!(send_user_confirmation_email: false)
- end
-
- it 'does not enqueue WrongullyConfirmedEmailUnconfirmer job' do
- Sidekiq::Testing.fake! do
- migrate!
-
- expect(BackgroundMigrationWorker.jobs.size).to eq(0)
- end
- end
- end
-
- context 'when email application setting record does not exist' do
- before do
- table(:application_settings).delete_all
- end
-
- it 'does not enqueue WrongullyConfirmedEmailUnconfirmer job' do
- Sidekiq::Testing.fake! do
- migrate!
-
- expect(BackgroundMigrationWorker.jobs.size).to eq(0)
- end
- end
- end
-end
diff --git a/spec/migrations/update_application_setting_npm_package_requests_forwarding_default_spec.rb b/spec/migrations/update_application_setting_npm_package_requests_forwarding_default_spec.rb
deleted file mode 100644
index be209536208..00000000000
--- a/spec/migrations/update_application_setting_npm_package_requests_forwarding_default_spec.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe UpdateApplicationSettingNpmPackageRequestsForwardingDefault do
- # Create test data - pipeline and CI/CD jobs.
- let(:application_settings) { table(:application_settings) }
-
- before do
- application_settings.create!(npm_package_requests_forwarding: false)
- end
-
- # Test just the up migration.
- it 'correctly migrates the application setting' do
- expect { migrate! }.to change { current_application_setting }.from(false).to(true)
- end
-
- # Test a reversible migration.
- it 'correctly migrates up and down the application setting' do
- reversible_migration do |migration|
- # Expectations will run before the up migration,
- # and then again after the down migration
- migration.before -> {
- expect(current_application_setting).to eq false
- }
-
- # Expectations will run after the up migration.
- migration.after -> {
- expect(current_application_setting).to eq true
- }
- end
- end
-
- def current_application_setting
- ApplicationSetting.current_without_cache.npm_package_requests_forwarding
- end
-end
diff --git a/spec/migrations/update_fingerprint_sha256_within_keys_spec.rb b/spec/migrations/update_fingerprint_sha256_within_keys_spec.rb
deleted file mode 100644
index 22ec3135703..00000000000
--- a/spec/migrations/update_fingerprint_sha256_within_keys_spec.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-RSpec.describe UpdateFingerprintSha256WithinKeys do
- let(:key_table) { table(:keys) }
-
- describe '#up' do
- it 'the BackgroundMigrationWorker will be triggered and fingerprint_sha256 populated' do
- key_table.create!(
- id: 1,
- user_id: 1,
- title: 'test',
- key: 'ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt1016k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=',
- fingerprint: 'ba:81:59:68:d7:6c:cd:02:02:bf:6a:9b:55:4e:af:d1',
- fingerprint_sha256: nil
- )
-
- expect(Key.first.fingerprint_sha256).to eq(nil)
-
- described_class.new.up
-
- expect(BackgroundMigrationWorker.jobs.size).to eq(1)
- expect(BackgroundMigrationWorker.jobs.first["args"][0]).to eq("MigrateFingerprintSha256WithinKeys")
- expect(BackgroundMigrationWorker.jobs.first["args"][1]).to eq([1, 1])
- end
- end
-end
diff --git a/spec/migrations/update_historical_data_recorded_at_spec.rb b/spec/migrations/update_historical_data_recorded_at_spec.rb
deleted file mode 100644
index 95d2bb989fd..00000000000
--- a/spec/migrations/update_historical_data_recorded_at_spec.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-RSpec.describe UpdateHistoricalDataRecordedAt do
- let(:historical_data_table) { table(:historical_data) }
-
- it 'reversibly populates recorded_at from created_at or date' do
- row1 = historical_data_table.create!(
- date: Date.current - 1.day,
- created_at: Time.current - 1.day
- )
-
- row2 = historical_data_table.create!(date: Date.current - 2.days)
- row2.update!(created_at: nil)
-
- reversible_migration do |migration|
- migration.before -> {
- expect(row1.reload.recorded_at).to eq(nil)
- expect(row2.reload.recorded_at).to eq(nil)
- }
-
- migration.after -> {
- expect(row1.reload.recorded_at).to eq(row1.created_at)
- expect(row2.reload.recorded_at).to eq(row2.date.in_time_zone(Time.zone).change(hour: 12))
- }
- end
- end
-end
diff --git a/spec/migrations/update_internal_ids_last_value_for_epics_renamed_spec.rb b/spec/migrations/update_internal_ids_last_value_for_epics_renamed_spec.rb
deleted file mode 100644
index d7d1781aaa2..00000000000
--- a/spec/migrations/update_internal_ids_last_value_for_epics_renamed_spec.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe UpdateInternalIdsLastValueForEpicsRenamed, :migration, schema: 20201124185639 do
- let(:namespaces) { table(:namespaces) }
- let(:users) { table(:users) }
- let(:epics) { table(:epics) }
- let(:internal_ids) { table(:internal_ids) }
-
- let!(:author) { users.create!(name: 'test', email: 'test@example.com', projects_limit: 0) }
- let!(:group1) { namespaces.create!(type: 'Group', name: 'group1', path: 'group1') }
- let!(:group2) { namespaces.create!(type: 'Group', name: 'group2', path: 'group2') }
- let!(:group3) { namespaces.create!(type: 'Group', name: 'group3', path: 'group3') }
- let!(:epic_last_value1) { internal_ids.create!(usage: 4, last_value: 5, namespace_id: group1.id) }
- let!(:epic_last_value2) { internal_ids.create!(usage: 4, last_value: 5, namespace_id: group2.id) }
- let!(:epic_last_value3) { internal_ids.create!(usage: 4, last_value: 5, namespace_id: group3.id) }
- let!(:epic_1) { epics.create!(iid: 110, title: 'from epic 1', group_id: group1.id, author_id: author.id, title_html: 'any') }
- let!(:epic_2) { epics.create!(iid: 5, title: 'from epic 1', group_id: group2.id, author_id: author.id, title_html: 'any') }
- let!(:epic_3) { epics.create!(iid: 3, title: 'from epic 1', group_id: group3.id, author_id: author.id, title_html: 'any') }
-
- it 'updates out of sync internal_ids last_value' do
- migrate!
-
- expect(internal_ids.find_by(usage: 4, namespace_id: group1.id).last_value).to eq(110)
- expect(internal_ids.find_by(usage: 4, namespace_id: group2.id).last_value).to eq(5)
- expect(internal_ids.find_by(usage: 4, namespace_id: group3.id).last_value).to eq(5)
- end
-end
diff --git a/spec/migrations/update_routes_for_lost_and_found_group_and_orphaned_projects_spec.rb b/spec/migrations/update_routes_for_lost_and_found_group_and_orphaned_projects_spec.rb
deleted file mode 100644
index 74e97b82363..00000000000
--- a/spec/migrations/update_routes_for_lost_and_found_group_and_orphaned_projects_spec.rb
+++ /dev/null
@@ -1,223 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-RSpec.describe UpdateRoutesForLostAndFoundGroupAndOrphanedProjects, :migration do
- let(:users) { table(:users) }
- let(:namespaces) { table(:namespaces) }
- let(:members) { table(:members) }
- let(:projects) { table(:projects) }
- let(:routes) { table(:routes) }
-
- before do
- # Create a Ghost User and its namnespace, but skip the route
- ghost_user = users.create!(
- name: 'Ghost User',
- username: 'ghost',
- email: 'ghost@example.com',
- user_type: described_class::User::USER_TYPE_GHOST,
- projects_limit: 100,
- state: :active,
- bio: 'This is a "Ghost User"'
- )
-
- namespaces.create!(
- name: 'Ghost User',
- path: 'ghost',
- owner_id: ghost_user.id,
- visibility_level: 20
- )
-
- # Create the 'lost-and-found', owned by the Ghost user, but with no route
- lost_and_found_group = namespaces.create!(
- name: described_class::User::LOST_AND_FOUND_GROUP,
- path: described_class::User::LOST_AND_FOUND_GROUP,
- type: 'Group',
- description: 'Group to store orphaned projects',
- visibility_level: 0
- )
-
- members.create!(
- type: 'GroupMember',
- source_id: lost_and_found_group.id,
- user_id: ghost_user.id,
- source_type: 'Namespace',
- access_level: described_class::User::ACCESS_LEVEL_OWNER,
- notification_level: 3
- )
-
- # Add an orphaned project under 'lost-and-found' but with the wrong path in its route
- orphaned_project = projects.create!(
- name: 'orphaned_project',
- path: 'orphaned_project',
- visibility_level: 20,
- archived: false,
- namespace_id: lost_and_found_group.id
- )
-
- routes.create!(
- source_id: orphaned_project.id,
- source_type: 'Project',
- path: 'orphaned_project',
- name: 'orphaned_project',
- created_at: Time.current,
- updated_at: Time.current
- )
-
- # Create another user named ghost which is not the Ghost User
- # Also create a 'lost-and-found' group for them and add projects to it
- # Purpose: test that the routes added for the 'lost-and-found' group and
- # its projects are unique
- fake_ghost_user = users.create!(
- name: 'Ghost User',
- username: 'ghost1',
- email: 'ghost1@example.com',
- user_type: nil,
- projects_limit: 100,
- state: :active,
- bio: 'This is NOT a "Ghost User"'
- )
-
- fake_ghost_user_namespace = namespaces.create!(
- name: 'Ghost User',
- path: 'ghost1',
- owner_id: fake_ghost_user.id,
- visibility_level: 20
- )
-
- routes.create!(
- source_id: fake_ghost_user_namespace.id,
- source_type: 'Namespace',
- path: 'ghost1',
- name: 'Ghost User',
- created_at: Time.current,
- updated_at: Time.current
- )
-
- fake_lost_and_found_group = namespaces.create!(
- name: 'Lost and Found',
- path: described_class::User::LOST_AND_FOUND_GROUP, # same path as the lost-and-found group
- type: 'Group',
- description: 'Fake lost and found group with the same path as the real one',
- visibility_level: 20
- )
-
- routes.create!(
- source_id: fake_lost_and_found_group.id,
- source_type: 'Namespace',
- path: described_class::User::LOST_AND_FOUND_GROUP, # same path as the lost-and-found group
- name: 'Lost and Found',
- created_at: Time.current,
- updated_at: Time.current
- )
-
- members.create!(
- type: 'GroupMember',
- source_id: fake_lost_and_found_group.id,
- user_id: fake_ghost_user.id,
- source_type: 'Namespace',
- access_level: described_class::User::ACCESS_LEVEL_OWNER,
- notification_level: 3
- )
-
- normal_project = projects.create!(
- name: 'normal_project',
- path: 'normal_project',
- visibility_level: 20,
- archived: false,
- namespace_id: fake_lost_and_found_group.id
- )
-
- routes.create!(
- source_id: normal_project.id,
- source_type: 'Project',
- path: "#{described_class::User::LOST_AND_FOUND_GROUP}/normal_project",
- name: 'Lost and Found / normal_project',
- created_at: Time.current,
- updated_at: Time.current
- )
-
- # Add a project whose route conflicts with the ghost username
- # and should force the data migration to pick a new Ghost username and path
- ghost_project = projects.create!(
- name: 'Ghost Project',
- path: 'ghost',
- visibility_level: 20,
- archived: false,
- namespace_id: fake_lost_and_found_group.id
- )
-
- routes.create!(
- source_id: ghost_project.id,
- source_type: 'Project',
- path: 'ghost',
- name: 'Ghost Project',
- created_at: Time.current,
- updated_at: Time.current
- )
- end
-
- it 'fixes the ghost user username and namespace path' do
- ghost_user = users.find_by(user_type: described_class::User::USER_TYPE_GHOST)
- ghost_namespace = namespaces.find_by(owner_id: ghost_user.id)
-
- expect(ghost_user.username).to eq('ghost')
- expect(ghost_namespace.path).to eq('ghost')
-
- disable_migrations_output { migrate! }
-
- ghost_user = users.find_by(user_type: described_class::User::USER_TYPE_GHOST)
- ghost_namespace = namespaces.find_by(owner_id: ghost_user.id)
- ghost_namespace_route = routes.find_by(source_id: ghost_namespace.id, source_type: 'Namespace')
-
- expect(ghost_user.username).to eq('ghost2')
- expect(ghost_namespace.path).to eq('ghost2')
- expect(ghost_namespace_route.path).to eq('ghost2')
- end
-
- it 'creates the route for the ghost user namespace' do
- expect(routes.where(path: 'ghost').count).to eq(1)
- expect(routes.where(path: 'ghost1').count).to eq(1)
- expect(routes.where(path: 'ghost2').count).to eq(0)
-
- disable_migrations_output { migrate! }
-
- expect(routes.where(path: 'ghost').count).to eq(1)
- expect(routes.where(path: 'ghost1').count).to eq(1)
- expect(routes.where(path: 'ghost2').count).to eq(1)
- end
-
- it 'fixes the path for the lost-and-found group by generating a unique one' do
- expect(namespaces.where(path: described_class::User::LOST_AND_FOUND_GROUP).count).to eq(2)
-
- disable_migrations_output { migrate! }
-
- expect(namespaces.where(path: described_class::User::LOST_AND_FOUND_GROUP).count).to eq(1)
-
- lost_and_found_group = namespaces.find_by(name: described_class::User::LOST_AND_FOUND_GROUP)
- expect(lost_and_found_group.path).to eq('lost-and-found1')
- end
-
- it 'creates the route for the lost-and-found group' do
- expect(routes.where(path: described_class::User::LOST_AND_FOUND_GROUP).count).to eq(1)
- expect(routes.where(path: 'lost-and-found1').count).to eq(0)
-
- disable_migrations_output { migrate! }
-
- expect(routes.where(path: described_class::User::LOST_AND_FOUND_GROUP).count).to eq(1)
- expect(routes.where(path: 'lost-and-found1').count).to eq(1)
- end
-
- it 'updates the route for the orphaned project' do
- orphaned_project_route = routes.find_by(path: 'orphaned_project')
- expect(orphaned_project_route.name).to eq('orphaned_project')
-
- disable_migrations_output { migrate! }
-
- updated_route = routes.find_by(id: orphaned_project_route.id)
- expect(updated_route.path).to eq('lost-and-found1/orphaned_project')
- expect(updated_route.name).to eq("#{described_class::User::LOST_AND_FOUND_GROUP} / orphaned_project")
- end
-end
diff --git a/spec/migrations/update_timestamp_softwarelicensespolicy_spec.rb b/spec/migrations/update_timestamp_softwarelicensespolicy_spec.rb
deleted file mode 100644
index 0210f23f5c5..00000000000
--- a/spec/migrations/update_timestamp_softwarelicensespolicy_spec.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-RSpec.describe UpdateTimestampSoftwarelicensespolicy do
- let(:software_licenses_policy) { table(:software_license_policies) }
- let(:projects) { table(:projects) }
- let(:licenses) { table(:software_licenses) }
-
- before do
- projects.create!(name: 'gitlab', path: 'gitlab-org/gitlab-ce', namespace_id: 1)
- licenses.create!(name: 'MIT')
- software_licenses_policy.create!(project_id: projects.first.id, software_license_id: licenses.first.id, created_at: nil, updated_at: nil)
- end
-
- it 'creates timestamps' do
- migrate!
-
- expect(software_licenses_policy.first.created_at).to be_present
- expect(software_licenses_policy.first.updated_at).to be_present
- end
-end
diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb
index e131661602e..bb8d476f257 100644
--- a/spec/models/ability_spec.rb
+++ b/spec/models/ability_spec.rb
@@ -425,9 +425,9 @@ RSpec.describe Ability do
expect(keys).to include(
:administrator,
'admin',
- "/dp/condition/BasePolicy/admin/#{user_b.id}"
+ "/dp/condition/BasePolicy/admin/User:#{user_b.id}"
)
- expect(keys).not_to include("/dp/condition/BasePolicy/admin/#{user_a.id}")
+ expect(keys).not_to include("/dp/condition/BasePolicy/admin/User:#{user_a.id}")
end
# regression spec for re-entrant admin condition checks
diff --git a/spec/models/acts_as_taggable_on/tag_spec.rb b/spec/models/acts_as_taggable_on/tag_spec.rb
new file mode 100644
index 00000000000..4b390bbd0bb
--- /dev/null
+++ b/spec/models/acts_as_taggable_on/tag_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ActsAsTaggableOn::Tag do
+ it 'has the same connection as Ci::ApplicationRecord' do
+ query = 'select current_database()'
+
+ expect(described_class.connection.execute(query).first).to eq(Ci::ApplicationRecord.connection.execute(query).first)
+ expect(described_class.retrieve_connection.execute(query).first).to eq(Ci::ApplicationRecord.retrieve_connection.execute(query).first)
+ end
+
+ it 'has the same sticking as Ci::ApplicationRecord' do
+ expect(described_class.sticking).to eq(Ci::ApplicationRecord.sticking)
+ end
+end
diff --git a/spec/models/acts_as_taggable_on/tagging_spec.rb b/spec/models/acts_as_taggable_on/tagging_spec.rb
new file mode 100644
index 00000000000..4520a0aaf70
--- /dev/null
+++ b/spec/models/acts_as_taggable_on/tagging_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ActsAsTaggableOn::Tagging do
+ it 'has the same connection as Ci::ApplicationRecord' do
+ query = 'select current_database()'
+
+ expect(described_class.connection.execute(query).first).to eq(Ci::ApplicationRecord.connection.execute(query).first)
+ expect(described_class.retrieve_connection.execute(query).first).to eq(Ci::ApplicationRecord.retrieve_connection.execute(query).first)
+ end
+
+ it 'has the same sticking as Ci::ApplicationRecord' do
+ expect(described_class.sticking).to eq(Ci::ApplicationRecord.sticking)
+ end
+end
diff --git a/spec/models/analytics/cycle_analytics/issue_stage_event_spec.rb b/spec/models/analytics/cycle_analytics/issue_stage_event_spec.rb
index c0d5b9203b8..ac17271ff99 100644
--- a/spec/models/analytics/cycle_analytics/issue_stage_event_spec.rb
+++ b/spec/models/analytics/cycle_analytics/issue_stage_event_spec.rb
@@ -9,5 +9,12 @@ RSpec.describe Analytics::CycleAnalytics::IssueStageEvent do
it { is_expected.to validate_presence_of(:project_id) }
it { is_expected.to validate_presence_of(:start_event_timestamp) }
- it_behaves_like 'StageEventModel'
+ it 'has state enum' do
+ expect(described_class.states).to eq(Issue.available_states)
+ end
+
+ it_behaves_like 'StageEventModel' do
+ let_it_be(:stage_event_factory) { :cycle_analytics_issue_stage_event }
+ let_it_be(:issuable_factory) { :issue }
+ end
end
diff --git a/spec/models/analytics/cycle_analytics/merge_request_stage_event_spec.rb b/spec/models/analytics/cycle_analytics/merge_request_stage_event_spec.rb
index 82a7e66d62a..bccc485d3f9 100644
--- a/spec/models/analytics/cycle_analytics/merge_request_stage_event_spec.rb
+++ b/spec/models/analytics/cycle_analytics/merge_request_stage_event_spec.rb
@@ -9,5 +9,12 @@ RSpec.describe Analytics::CycleAnalytics::MergeRequestStageEvent do
it { is_expected.to validate_presence_of(:project_id) }
it { is_expected.to validate_presence_of(:start_event_timestamp) }
- it_behaves_like 'StageEventModel'
+ it 'has state enum' do
+ expect(described_class.states).to eq(MergeRequest.available_states)
+ end
+
+ it_behaves_like 'StageEventModel' do
+ let_it_be(:stage_event_factory) { :cycle_analytics_merge_request_stage_event }
+ let_it_be(:issuable_factory) { :merge_request }
+ end
end
diff --git a/spec/models/blob_viewer/package_json_spec.rb b/spec/models/blob_viewer/package_json_spec.rb
index 8a394a7334f..1dcba3bcb4f 100644
--- a/spec/models/blob_viewer/package_json_spec.rb
+++ b/spec/models/blob_viewer/package_json_spec.rb
@@ -27,11 +27,55 @@ RSpec.describe BlobViewer::PackageJson do
end
end
- describe '#package_url' do
- it 'returns the package URL' do
- expect(subject).to receive(:prepare!)
+ context 'yarn' do
+ let(:data) do
+ <<-SPEC.strip_heredoc
+ {
+ "name": "module-name",
+ "version": "10.3.1",
+ "engines": {
+ "yarn": "^2.4.0"
+ }
+ }
+ SPEC
+ end
+
+ let(:blob) { fake_blob(path: 'package.json', data: data) }
+
+ subject { described_class.new(blob) }
+
+ describe '#package_url' do
+ it 'returns the package URL', :aggregate_failures do
+ expect(subject).to receive(:prepare!)
+
+ expect(subject.package_url).to eq("https://yarnpkg.com/package/#{subject.package_name}")
+ end
+ end
- expect(subject.package_url).to eq("https://www.npmjs.com/package/#{subject.package_name}")
+ describe '#manager_url' do
+ it 'returns the manager URL', :aggregate_failures do
+ expect(subject).to receive(:prepare!)
+
+ expect(subject.manager_url).to eq("https://yarnpkg.com/")
+ end
+ end
+ end
+
+ context 'npm' do
+ describe '#package_url' do
+ it 'returns the package URL', :aggregate_failures do
+ expect(subject).to receive(:prepare!)
+
+ expect(subject.package_url).to eq("https://www.npmjs.com/package/#{subject.package_name}")
+ end
+ end
+
+ describe '#manager_url' do
+ it 'returns the manager URL', :aggregate_failures do
+ expect(subject).to receive(:prepare!)
+
+ expect(subject.manager_url).to eq("https://www.npmjs.com/")
+ end
end
end
diff --git a/spec/models/bulk_imports/entity_spec.rb b/spec/models/bulk_imports/entity_spec.rb
index 278d7f4bc56..cc66572cd6f 100644
--- a/spec/models/bulk_imports/entity_spec.rb
+++ b/spec/models/bulk_imports/entity_spec.rb
@@ -243,4 +243,13 @@ RSpec.describe BulkImports::Entity, type: :model do
end
end
end
+
+ describe '#relation_download_url_path' do
+ it 'returns export relations url with download query string' do
+ entity = build(:bulk_import_entity)
+
+ expect(entity.relation_download_url_path('test'))
+ .to eq("/groups/#{entity.encoded_source_full_path}/export_relations/download?relation=test")
+ end
+ end
end
diff --git a/spec/models/bulk_imports/file_transfer/project_config_spec.rb b/spec/models/bulk_imports/file_transfer/project_config_spec.rb
index 3bd79333f0c..02151da583e 100644
--- a/spec/models/bulk_imports/file_transfer/project_config_spec.rb
+++ b/spec/models/bulk_imports/file_transfer/project_config_spec.rb
@@ -80,7 +80,7 @@ RSpec.describe BulkImports::FileTransfer::ProjectConfig do
describe '#tree_relation_definition_for' do
it 'returns relation definition' do
- expected = { service_desk_setting: { except: [:outgoing_name, :file_template_project_id], include: [] } }
+ expected = { service_desk_setting: { except: [:outgoing_name, :file_template_project_id], include: [], only: %i[project_id issue_template_key project_key] } }
expect(subject.tree_relation_definition_for('service_desk_setting')).to eq(expected)
end
diff --git a/spec/models/chat_name_spec.rb b/spec/models/chat_name_spec.rb
index 9ed00003ac1..67e0f98d147 100644
--- a/spec/models/chat_name_spec.rb
+++ b/spec/models/chat_name_spec.rb
@@ -43,4 +43,12 @@ RSpec.describe ChatName do
expect(subject.last_used_at).to eq(time)
end
end
+
+ it_behaves_like 'it has loose foreign keys' do
+ let(:factory_name) { :chat_name }
+
+ before do
+ Ci::PipelineChatData # ensure that the referenced model is loaded
+ end
+ end
end
diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb
index 8f1ae9c5f02..6fde55103f8 100644
--- a/spec/models/ci/bridge_spec.rb
+++ b/spec/models/ci/bridge_spec.rb
@@ -17,8 +17,6 @@ RSpec.describe Ci::Bridge do
{ trigger: { project: 'my/project', branch: 'master' } }
end
- it { is_expected.to respond_to(:runner_features) }
-
it 'has many sourced pipelines' do
expect(bridge).to have_many(:sourced_pipelines)
end
diff --git a/spec/models/ci/build_metadata_spec.rb b/spec/models/ci/build_metadata_spec.rb
index 069864fa765..b2ffb34da1d 100644
--- a/spec/models/ci/build_metadata_spec.rb
+++ b/spec/models/ci/build_metadata_spec.rb
@@ -121,4 +121,16 @@ RSpec.describe Ci::BuildMetadata do
end
end
end
+
+ describe 'set_cancel_gracefully' do
+ it 'sets cancel_gracefully' do
+ build.set_cancel_gracefully
+
+ expect(build.cancel_gracefully?).to be true
+ end
+
+ it 'returns false' do
+ expect(build.cancel_gracefully?).to be false
+ end
+ end
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 2ebf75a1d8a..b7de8ca4337 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -35,7 +35,8 @@ RSpec.describe Ci::Build do
it { is_expected.to respond_to(:has_trace?) }
it { is_expected.to respond_to(:trace) }
- it { is_expected.to respond_to(:runner_features) }
+ it { is_expected.to respond_to(:set_cancel_gracefully) }
+ it { is_expected.to respond_to(:cancel_gracefully?) }
it { is_expected.to delegate_method(:merge_request?).to(:pipeline) }
it { is_expected.to delegate_method(:merge_request_ref?).to(:pipeline) }
@@ -214,6 +215,26 @@ RSpec.describe Ci::Build do
end
end
+ describe '.license_management_jobs' do
+ subject { described_class.license_management_jobs }
+
+ let!(:management_build) { create(:ci_build, :success, name: :license_management) }
+ let!(:scanning_build) { create(:ci_build, :success, name: :license_scanning) }
+ let!(:another_build) { create(:ci_build, :success, name: :another_type) }
+
+ it 'returns license_scanning jobs' do
+ is_expected.to include(scanning_build)
+ end
+
+ it 'returns license_management jobs' do
+ is_expected.to include(management_build)
+ end
+
+ it 'doesnt return filtered out jobs' do
+ is_expected.not_to include(another_build)
+ end
+ end
+
describe '.finished_before' do
subject { described_class.finished_before(date) }
@@ -350,7 +371,7 @@ RSpec.describe Ci::Build do
it 'sticks the build if the status changed' do
job = create(:ci_build, :pending)
- expect(ApplicationRecord.sticking).to receive(:stick)
+ expect(described_class.sticking).to receive(:stick)
.with(:build, job.id)
job.update!(status: :running)
@@ -1290,7 +1311,7 @@ RSpec.describe Ci::Build do
end
end
- shared_examples_for 'state transition as a deployable' do
+ describe 'state transition as a deployable' do
subject { build.send(event) }
let!(:build) { create(:ci_build, :with_deployment, :start_review_app, project: project, pipeline: pipeline) }
@@ -1332,6 +1353,22 @@ RSpec.describe Ci::Build do
expect(deployment).to be_running
end
+
+ context 'when deployment is already running state' do
+ before do
+ build.deployment.success!
+ end
+
+ it 'does not change deployment status and tracks an error' do
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception).with(
+ instance_of(Deployment::StatusSyncError), deployment_id: deployment.id, build_id: build.id)
+
+ with_cross_database_modification_prevented do
+ expect { subject }.not_to change { deployment.reload.status }
+ end
+ end
+ end
end
context 'when transits to success' do
@@ -1399,36 +1436,6 @@ RSpec.describe Ci::Build do
end
end
- it_behaves_like 'state transition as a deployable' do
- context 'when transits to running' do
- let(:event) { :run! }
-
- context 'when deployment is already running state' do
- before do
- build.deployment.success!
- end
-
- it 'does not change deployment status and tracks an error' do
- expect(Gitlab::ErrorTracking)
- .to receive(:track_exception).with(
- instance_of(Deployment::StatusSyncError), deployment_id: deployment.id, build_id: build.id)
-
- with_cross_database_modification_prevented do
- expect { subject }.not_to change { deployment.reload.status }
- end
- end
- end
- end
- end
-
- context 'when update_deployment_after_transaction_commit feature flag is disabled' do
- before do
- stub_feature_flags(update_deployment_after_transaction_commit: false)
- end
-
- it_behaves_like 'state transition as a deployable'
- end
-
describe '#on_stop' do
subject { build.on_stop }
@@ -2759,7 +2766,10 @@ RSpec.describe Ci::Build do
let(:job_dependency_var) { { key: 'job_dependency', value: 'value', public: true, masked: false } }
before do
- allow(build).to receive(:predefined_variables) { [build_pre_var] }
+ allow_next_instance_of(Gitlab::Ci::Variables::Builder) do |builder|
+ allow(builder).to receive(:predefined_variables) { [build_pre_var] }
+ end
+
allow(build).to receive(:yaml_variables) { [build_yaml_var] }
allow(build).to receive(:persisted_variables) { [] }
allow(build).to receive(:job_jwt_variables) { [job_jwt_var] }
@@ -3411,75 +3421,122 @@ RSpec.describe Ci::Build do
end
describe '#scoped_variables' do
- context 'when build has not been persisted yet' do
- let(:build) do
- described_class.new(
- name: 'rspec',
- stage: 'test',
- ref: 'feature',
- project: project,
- pipeline: pipeline,
- scheduling_type: :stage
- )
- end
+ before do
+ pipeline.clear_memoization(:predefined_vars_in_builder_enabled)
+ end
- let(:pipeline) { create(:ci_pipeline, project: project, ref: 'feature') }
+ it 'records a prometheus metric' do
+ histogram = double(:histogram)
+ expect(::Gitlab::Ci::Pipeline::Metrics).to receive(:pipeline_builder_scoped_variables_histogram)
+ .and_return(histogram)
- it 'does not persist the build' do
- expect(build).to be_valid
- expect(build).not_to be_persisted
+ expect(histogram).to receive(:observe)
+ .with({}, a_kind_of(ActiveSupport::Duration))
- build.scoped_variables
+ build.scoped_variables
+ end
- expect(build).not_to be_persisted
- end
+ shared_examples 'calculates scoped_variables' do
+ context 'when build has not been persisted yet' do
+ let(:build) do
+ described_class.new(
+ name: 'rspec',
+ stage: 'test',
+ ref: 'feature',
+ project: project,
+ pipeline: pipeline,
+ scheduling_type: :stage
+ )
+ end
- it 'returns static predefined variables' do
- keys = %w[CI_JOB_NAME
- CI_COMMIT_SHA
- CI_COMMIT_SHORT_SHA
- CI_COMMIT_REF_NAME
- CI_COMMIT_REF_SLUG
- CI_JOB_STAGE]
+ let(:pipeline) { create(:ci_pipeline, project: project, ref: 'feature') }
- variables = build.scoped_variables
+ it 'does not persist the build' do
+ expect(build).to be_valid
+ expect(build).not_to be_persisted
- variables.map { |env| env[:key] }.tap do |names|
- expect(names).to include(*keys)
+ build.scoped_variables
+
+ expect(build).not_to be_persisted
end
- expect(variables)
- .to include(key: 'CI_COMMIT_REF_NAME', value: 'feature', public: true, masked: false)
+ it 'returns static predefined variables' do
+ keys = %w[CI_JOB_NAME
+ CI_COMMIT_SHA
+ CI_COMMIT_SHORT_SHA
+ CI_COMMIT_REF_NAME
+ CI_COMMIT_REF_SLUG
+ CI_JOB_STAGE]
+
+ variables = build.scoped_variables
+
+ variables.map { |env| env[:key] }.tap do |names|
+ expect(names).to include(*keys)
+ end
+
+ expect(variables)
+ .to include(key: 'CI_COMMIT_REF_NAME', value: 'feature', public: true, masked: false)
+ end
+
+ it 'does not return prohibited variables' do
+ keys = %w[CI_JOB_ID
+ CI_JOB_URL
+ CI_JOB_TOKEN
+ CI_BUILD_ID
+ CI_BUILD_TOKEN
+ CI_REGISTRY_USER
+ CI_REGISTRY_PASSWORD
+ CI_REPOSITORY_URL
+ CI_ENVIRONMENT_URL
+ CI_DEPLOY_USER
+ CI_DEPLOY_PASSWORD]
+
+ build.scoped_variables.map { |env| env[:key] }.tap do |names|
+ expect(names).not_to include(*keys)
+ end
+ end
end
- it 'does not return prohibited variables' do
- keys = %w[CI_JOB_ID
- CI_JOB_URL
- CI_JOB_TOKEN
- CI_BUILD_ID
- CI_BUILD_TOKEN
- CI_REGISTRY_USER
- CI_REGISTRY_PASSWORD
- CI_REPOSITORY_URL
- CI_ENVIRONMENT_URL
- CI_DEPLOY_USER
- CI_DEPLOY_PASSWORD]
+ context 'with dependency variables' do
+ let!(:prepare) { create(:ci_build, name: 'prepare', pipeline: pipeline, stage_idx: 0) }
+ let!(:build) { create(:ci_build, pipeline: pipeline, stage_idx: 1, options: { dependencies: ['prepare'] }) }
+
+ let!(:job_variable) { create(:ci_job_variable, :dotenv_source, job: prepare) }
- build.scoped_variables.map { |env| env[:key] }.tap do |names|
- expect(names).not_to include(*keys)
+ it 'inherits dependent variables' do
+ expect(build.scoped_variables.to_hash).to include(job_variable.key => job_variable.value)
end
end
end
- context 'with dependency variables' do
- let!(:prepare) { create(:ci_build, name: 'prepare', pipeline: pipeline, stage_idx: 0) }
- let!(:build) { create(:ci_build, pipeline: pipeline, stage_idx: 1, options: { dependencies: ['prepare'] }) }
+ it_behaves_like 'calculates scoped_variables'
- let!(:job_variable) { create(:ci_job_variable, :dotenv_source, job: prepare) }
+ it 'delegates to the variable builders' do
+ expect_next_instance_of(Gitlab::Ci::Variables::Builder) do |builder|
+ expect(builder)
+ .to receive(:scoped_variables).with(build, hash_including(:environment, :dependencies))
+ .and_call_original
- it 'inherits dependent variables' do
- expect(build.scoped_variables.to_hash).to include(job_variable.key => job_variable.value)
+ expect(builder).to receive(:predefined_variables).and_call_original
end
+
+ build.scoped_variables
+ end
+
+ context 'when ci builder feature flag is disabled' do
+ before do
+ stub_feature_flags(ci_predefined_vars_in_builder: false)
+ end
+
+ it 'does not delegate to the variable builders' do
+ expect_next_instance_of(Gitlab::Ci::Variables::Builder) do |builder|
+ expect(builder).not_to receive(:predefined_variables)
+ end
+
+ build.scoped_variables
+ end
+
+ it_behaves_like 'calculates scoped_variables'
end
end
@@ -3569,6 +3626,27 @@ RSpec.describe Ci::Build do
include_examples "secret CI variables"
end
+ describe '#kubernetes_variables' do
+ let(:build) { create(:ci_build) }
+ let(:service) { double(execute: template) }
+ let(:template) { double(to_yaml: 'example-kubeconfig', valid?: template_valid) }
+ let(:template_valid) { true }
+
+ subject { build.kubernetes_variables }
+
+ before do
+ allow(Ci::GenerateKubeconfigService).to receive(:new).with(build).and_return(service)
+ end
+
+ it { is_expected.to include(key: 'KUBECONFIG', value: 'example-kubeconfig', public: false, file: true) }
+
+ context 'generated config is invalid' do
+ let(:template_valid) { false }
+
+ it { is_expected.not_to include(key: 'KUBECONFIG', value: 'example-kubeconfig', public: false, file: true) }
+ end
+ end
+
describe '#deployment_variables' do
let(:build) { create(:ci_build, environment: environment) }
let(:environment) { 'production' }
@@ -3728,7 +3806,7 @@ RSpec.describe Ci::Build do
it 'ensures that it is not run in database transaction' do
expect(job.pipeline.persistent_ref).to receive(:create) do
- expect(Gitlab::Database.main).not_to be_inside_transaction
+ expect(ApplicationRecord).not_to be_inside_transaction
end
run_job_without_exception
@@ -5326,4 +5404,23 @@ RSpec.describe Ci::Build do
create(:ci_build)
end
end
+
+ describe '#runner_features' do
+ subject do
+ build.save!
+ build.cancel_gracefully?
+ end
+
+ let_it_be(:build) { create(:ci_build, pipeline: pipeline) }
+
+ it 'cannot cancel gracefully' do
+ expect(subject).to be false
+ end
+
+ it 'can cancel gracefully' do
+ build.set_cancel_gracefully
+
+ expect(subject).to be true
+ end
+ end
end
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index a94a1dd284a..d63f87e8943 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -351,6 +351,21 @@ RSpec.describe Ci::JobArtifact do
end
end
+ context 'when updating any field except the file' do
+ let(:artifact) { create(:ci_job_artifact, :unarchived_trace_artifact, file_store: 2) }
+
+ before do
+ stub_artifacts_object_storage(direct_upload: true)
+ artifact.file.object_store = 1
+ end
+
+ it 'the `after_commit` hook does not update `file_store`' do
+ artifact.update!(expire_at: Time.current)
+
+ expect(artifact.file_store).to be(2)
+ end
+ end
+
describe 'validates file format' do
subject { artifact }
@@ -507,6 +522,53 @@ RSpec.describe Ci::JobArtifact do
end
end
+ describe '#store_after_commit?' do
+ let(:file_type) { :archive }
+ let(:artifact) { build(:ci_job_artifact, file_type) }
+
+ context 'when direct upload is enabled' do
+ before do
+ stub_artifacts_object_storage(direct_upload: true)
+ end
+
+ context 'when the artifact is a trace' do
+ let(:file_type) { :trace }
+
+ context 'when ci_store_trace_outside_transaction is enabled' do
+ it 'returns true' do
+ expect(artifact.store_after_commit?).to be_truthy
+ end
+ end
+
+ context 'when ci_store_trace_outside_transaction is disabled' do
+ before do
+ stub_feature_flags(ci_store_trace_outside_transaction: false)
+ end
+
+ it 'returns false' do
+ expect(artifact.store_after_commit?).to be_falsey
+ end
+ end
+ end
+
+ context 'when the artifact is not a trace' do
+ it 'returns false' do
+ expect(artifact.store_after_commit?).to be_falsey
+ end
+ end
+ end
+
+ context 'when direct upload is disabled' do
+ before do
+ stub_artifacts_object_storage(direct_upload: false)
+ end
+
+ it 'returns false' do
+ expect(artifact.store_after_commit?).to be_falsey
+ end
+ end
+ end
+
describe 'file is being stored' do
subject { create(:ci_job_artifact, :archive) }
diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb
index c7e1fe91b1e..fee74f8f674 100644
--- a/spec/models/ci/pipeline_schedule_spec.rb
+++ b/spec/models/ci/pipeline_schedule_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Ci::PipelineSchedule do
- let_it_be(:project) { create_default(:project) }
+ let_it_be_with_reload(:project) { create_default(:project) }
subject { build(:ci_pipeline_schedule) }
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 5f3aad0ab24..e573a6ef780 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -3361,7 +3361,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
shared_examples 'sending a notification' do
it 'sends an email', :sidekiq_might_not_need_inline do
- should_only_email(pipeline.user, kind: :bcc)
+ should_only_email(pipeline.user)
end
end
@@ -4595,4 +4595,20 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
end
end
+
+ describe '#authorized_cluster_agents' do
+ let(:pipeline) { create(:ci_empty_pipeline, :created) }
+ let(:agent) { instance_double(Clusters::Agent) }
+ let(:authorization) { instance_double(Clusters::Agents::GroupAuthorization, agent: agent) }
+ let(:finder) { double(execute: [authorization]) }
+
+ it 'retrieves agent records from the finder and caches the result' do
+ expect(Clusters::AgentAuthorizationsFinder).to receive(:new).once
+ .with(pipeline.project)
+ .and_return(finder)
+
+ expect(pipeline.authorized_cluster_agents).to contain_exactly(agent)
+ expect(pipeline.authorized_cluster_agents).to contain_exactly(agent) # cached
+ end
+ end
end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 826332268c5..2e79159cc60 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -5,6 +5,14 @@ require 'spec_helper'
RSpec.describe Ci::Runner do
it_behaves_like 'having unique enum values'
+ it_behaves_like 'it has loose foreign keys' do
+ let(:factory_name) { :ci_runner }
+
+ before do
+ Clusters::Applications::Runner # ensure that the referenced model is loaded
+ end
+ end
+
describe 'groups association' do
# Due to other assoctions such as projects this whole spec is allowed to
# generate cross-database queries. So we have this temporary spec to
@@ -44,7 +52,7 @@ RSpec.describe Ci::Runner do
let(:runner) { create(:ci_runner, :group, groups: [group]) }
it 'disallows assigning group if already assigned to a group' do
- runner.groups << build(:group)
+ runner.runner_namespaces << build(:ci_runner_namespace)
expect(runner).not_to be_valid
expect(runner.errors.full_messages).to include('Runner needs to be assigned to exactly one group')
@@ -397,7 +405,7 @@ RSpec.describe Ci::Runner do
it 'sticks the runner to the primary and calls the original method' do
runner = create(:ci_runner)
- expect(ApplicationRecord.sticking).to receive(:stick)
+ expect(described_class.sticking).to receive(:stick)
.with(:runner, runner.id)
expect(Gitlab::Workhorse).to receive(:set_key_and_notify)
@@ -618,7 +626,7 @@ RSpec.describe Ci::Runner do
end
describe '#status' do
- let(:runner) { create(:ci_runner, :instance, contacted_at: 1.second.ago) }
+ let(:runner) { build(:ci_runner, :instance) }
subject { runner.status }
@@ -630,6 +638,45 @@ RSpec.describe Ci::Runner do
it { is_expected.to eq(:not_connected) }
end
+ context 'inactive but online' do
+ before do
+ runner.contacted_at = 1.second.ago
+ runner.active = false
+ end
+
+ it { is_expected.to eq(:online) }
+ end
+
+ context 'contacted 1s ago' do
+ before do
+ runner.contacted_at = 1.second.ago
+ end
+
+ it { is_expected.to eq(:online) }
+ end
+
+ context 'contacted long time ago' do
+ before do
+ runner.contacted_at = 1.year.ago
+ end
+
+ it { is_expected.to eq(:offline) }
+ end
+ end
+
+ describe '#deprecated_rest_status' do
+ let(:runner) { build(:ci_runner, :instance, contacted_at: 1.second.ago) }
+
+ subject { runner.deprecated_rest_status }
+
+ context 'never connected' do
+ before do
+ runner.contacted_at = nil
+ end
+
+ it { is_expected.to eq(:not_connected) }
+ end
+
context 'contacted 1s ago' do
before do
runner.contacted_at = 1.second.ago
diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb
index 4ba6c6e50f7..c254279a32f 100644
--- a/spec/models/ci/trigger_spec.rb
+++ b/spec/models/ci/trigger_spec.rb
@@ -57,4 +57,8 @@ RSpec.describe Ci::Trigger do
it { is_expected.to eq(false) }
end
end
+
+ it_behaves_like 'includes Limitable concern' do
+ subject { build(:ci_trigger, owner: project.owner, project: project) }
+ end
end
diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb
index 788430d53d3..806c60d5aff 100644
--- a/spec/models/clusters/applications/runner_spec.rb
+++ b/spec/models/clusters/applications/runner_spec.rb
@@ -112,7 +112,7 @@ RSpec.describe Clusters::Applications::Runner do
subject
expect(runner).to be_group_type
- expect(runner.groups).to eq [group]
+ expect(runner.runner_namespaces.pluck(:namespace_id)).to match_array [group.id]
end
end
@@ -162,12 +162,12 @@ RSpec.describe Clusters::Applications::Runner do
it 'pauses associated runner' do
active_runner = create(:ci_runner, contacted_at: 1.second.ago)
- expect(active_runner.status).to eq(:online)
+ expect(active_runner.active).to be_truthy
application_runner = create(:clusters_applications_runner, :scheduled, runner: active_runner)
application_runner.prepare_uninstall
- expect(active_runner.status).to eq(:paused)
+ expect(active_runner.active).to be_falsey
end
end
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index 9d305e31bad..d61bed80aaa 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -178,13 +178,13 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
end
end
- describe '.with_application_prometheus' do
- subject { described_class.with_application_prometheus }
+ describe '.with_integration_prometheus' do
+ subject { described_class.with_integration_prometheus }
let!(:cluster) { create(:cluster) }
context 'cluster has prometheus application' do
- let!(:application) { create(:clusters_applications_prometheus, :installed, cluster: cluster) }
+ let!(:application) { create(:clusters_integrations_prometheus, cluster: cluster) }
it { is_expected.to include(cluster) }
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 20afddd8470..59d14574c02 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -379,6 +379,22 @@ RSpec.describe CommitStatus do
end
end
+ describe '.retried_ordered' do
+ subject { described_class.retried_ordered.to_a }
+
+ let!(:statuses) do
+ [create_status(name: 'aa', ref: 'bb', status: 'running', retried: true),
+ create_status(name: 'cc', ref: 'cc', status: 'pending', retried: true),
+ create_status(name: 'aa', ref: 'cc', status: 'success', retried: true),
+ create_status(name: 'cc', ref: 'bb', status: 'success'),
+ create_status(name: 'aa', ref: 'bb', status: 'success')]
+ end
+
+ it 'returns retried statuses in order' do
+ is_expected.to eq(statuses.values_at(2, 0, 1))
+ end
+ end
+
describe '.running_or_pending' do
subject { described_class.running_or_pending.order(:id) }
diff --git a/spec/models/concerns/bulk_insert_safe_spec.rb b/spec/models/concerns/bulk_insert_safe_spec.rb
index 172986c142c..e6b197f34ca 100644
--- a/spec/models/concerns/bulk_insert_safe_spec.rb
+++ b/spec/models/concerns/bulk_insert_safe_spec.rb
@@ -5,42 +5,42 @@ require 'spec_helper'
RSpec.describe BulkInsertSafe do
before(:all) do
ActiveRecord::Schema.define do
- create_table :bulk_insert_parent_items, force: true do |t|
+ create_table :_test_bulk_insert_parent_items, force: true do |t|
t.string :name, null: false
end
- create_table :bulk_insert_items, force: true do |t|
+ create_table :_test_bulk_insert_items, force: true do |t|
t.string :name, null: true
t.integer :enum_value, null: false
t.text :encrypted_secret_value, null: false
t.string :encrypted_secret_value_iv, null: false
t.binary :sha_value, null: false, limit: 20
t.jsonb :jsonb_value, null: false
- t.belongs_to :bulk_insert_parent_item, foreign_key: true, null: true
+ t.belongs_to :bulk_insert_parent_item, foreign_key: { to_table: :_test_bulk_insert_parent_items }, null: true
t.timestamps null: true
t.index :name, unique: true
end
- create_table :bulk_insert_items_with_composite_pk, id: false, force: true do |t|
+ create_table :_test_bulk_insert_items_with_composite_pk, id: false, force: true do |t|
t.integer :id, null: true
t.string :name, null: true
end
- execute("ALTER TABLE bulk_insert_items_with_composite_pk ADD PRIMARY KEY (id,name);")
+ execute("ALTER TABLE _test_bulk_insert_items_with_composite_pk ADD PRIMARY KEY (id,name);")
end
end
after(:all) do
ActiveRecord::Schema.define do
- drop_table :bulk_insert_items, force: true
- drop_table :bulk_insert_parent_items, force: true
- drop_table :bulk_insert_items_with_composite_pk, force: true
+ drop_table :_test_bulk_insert_items, force: true
+ drop_table :_test_bulk_insert_parent_items, force: true
+ drop_table :_test_bulk_insert_items_with_composite_pk, force: true
end
end
BulkInsertParentItem = Class.new(ActiveRecord::Base) do
- self.table_name = :bulk_insert_parent_items
+ self.table_name = :_test_bulk_insert_parent_items
self.inheritance_column = :_type_disabled
def self.name
@@ -54,7 +54,7 @@ RSpec.describe BulkInsertSafe do
let_it_be(:bulk_insert_item_class) do
Class.new(ActiveRecord::Base) do
- self.table_name = 'bulk_insert_items'
+ self.table_name = '_test_bulk_insert_items'
include BulkInsertSafe
include ShaAttribute
@@ -182,7 +182,7 @@ RSpec.describe BulkInsertSafe do
context 'with returns option set' do
let(:items) { bulk_insert_item_class.valid_list(1) }
- subject(:bulk_insert) { bulk_insert_item_class.bulk_insert!(items, returns: returns) }
+ subject(:legacy_bulk_insert) { bulk_insert_item_class.bulk_insert!(items, returns: returns) }
context 'when is set to :ids' do
let(:returns) { :ids }
@@ -247,7 +247,7 @@ RSpec.describe BulkInsertSafe do
context 'when a model with composite primary key is inserted' do
let_it_be(:bulk_insert_items_with_composite_pk_class) do
Class.new(ActiveRecord::Base) do
- self.table_name = 'bulk_insert_items_with_composite_pk'
+ self.table_name = '_test_bulk_insert_items_with_composite_pk'
include BulkInsertSafe
end
diff --git a/spec/models/concerns/bulk_insertable_associations_spec.rb b/spec/models/concerns/bulk_insertable_associations_spec.rb
index 25b13c8233d..9713f1ce9a4 100644
--- a/spec/models/concerns/bulk_insertable_associations_spec.rb
+++ b/spec/models/concerns/bulk_insertable_associations_spec.rb
@@ -6,42 +6,50 @@ RSpec.describe BulkInsertableAssociations do
class BulkFoo < ApplicationRecord
include BulkInsertSafe
+ self.table_name = '_test_bulk_foos'
+
validates :name, presence: true
end
class BulkBar < ApplicationRecord
include BulkInsertSafe
+
+ self.table_name = '_test_bulk_bars'
end
- SimpleBar = Class.new(ApplicationRecord)
+ SimpleBar = Class.new(ApplicationRecord) do
+ self.table_name = '_test_simple_bars'
+ end
class BulkParent < ApplicationRecord
include BulkInsertableAssociations
- has_many :bulk_foos
+ self.table_name = '_test_bulk_parents'
+
+ has_many :bulk_foos, class_name: 'BulkFoo'
has_many :bulk_hunks, class_name: 'BulkFoo'
- has_many :bulk_bars
- has_many :simple_bars # not `BulkInsertSafe`
+ has_many :bulk_bars, class_name: 'BulkBar'
+ has_many :simple_bars, class_name: 'SimpleBar' # not `BulkInsertSafe`
has_one :bulk_foo # not supported
end
before(:all) do
ActiveRecord::Schema.define do
- create_table :bulk_parents, force: true do |t|
+ create_table :_test_bulk_parents, force: true do |t|
t.string :name, null: true
end
- create_table :bulk_foos, force: true do |t|
+ create_table :_test_bulk_foos, force: true do |t|
t.string :name, null: true
t.belongs_to :bulk_parent, null: false
end
- create_table :bulk_bars, force: true do |t|
+ create_table :_test_bulk_bars, force: true do |t|
t.string :name, null: true
t.belongs_to :bulk_parent, null: false
end
- create_table :simple_bars, force: true do |t|
+ create_table :_test_simple_bars, force: true do |t|
t.string :name, null: true
t.belongs_to :bulk_parent, null: false
end
@@ -50,10 +58,10 @@ RSpec.describe BulkInsertableAssociations do
after(:all) do
ActiveRecord::Schema.define do
- drop_table :bulk_foos, force: true
- drop_table :bulk_bars, force: true
- drop_table :simple_bars, force: true
- drop_table :bulk_parents, force: true
+ drop_table :_test_bulk_foos, force: true
+ drop_table :_test_bulk_bars, force: true
+ drop_table :_test_simple_bars, force: true
+ drop_table :_test_bulk_parents, force: true
end
end
diff --git a/spec/models/concerns/cascading_namespace_setting_attribute_spec.rb b/spec/models/concerns/cascading_namespace_setting_attribute_spec.rb
index e8f2b18e662..6be6e3f048f 100644
--- a/spec/models/concerns/cascading_namespace_setting_attribute_spec.rb
+++ b/spec/models/concerns/cascading_namespace_setting_attribute_spec.rb
@@ -136,6 +136,21 @@ RSpec.describe NamespaceSetting, 'CascadingNamespaceSettingAttribute' do
.to raise_error(ActiveRecord::RecordInvalid, /Delayed project removal cannot be changed because it is locked by an ancestor/)
end
end
+
+ context 'when parent locked the attribute then the application settings locks it' do
+ before do
+ subgroup_settings.update!(delayed_project_removal: true)
+ group_settings.update!(lock_delayed_project_removal: true, delayed_project_removal: false)
+ stub_application_setting(lock_delayed_project_removal: true, delayed_project_removal: true)
+
+ subgroup_settings.clear_memoization(:delayed_project_removal)
+ subgroup_settings.clear_memoization(:delayed_project_removal_locked_ancestor)
+ end
+
+ it 'returns the application setting value' do
+ expect(delayed_project_removal).to eq(true)
+ end
+ end
end
describe '#delayed_project_removal?' do
diff --git a/spec/models/concerns/clusters/agents/authorization_config_scopes_spec.rb b/spec/models/concerns/clusters/agents/authorization_config_scopes_spec.rb
new file mode 100644
index 00000000000..a4d1a33b3d5
--- /dev/null
+++ b/spec/models/concerns/clusters/agents/authorization_config_scopes_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Clusters::Agents::AuthorizationConfigScopes do
+ describe '.with_available_ci_access_fields' do
+ let(:project) { create(:project) }
+
+ let!(:agent_authorization_0) { create(:agent_project_authorization, project: project) }
+ let!(:agent_authorization_1) { create(:agent_project_authorization, project: project, config: { access_as: {} }) }
+ let!(:agent_authorization_2) { create(:agent_project_authorization, project: project, config: { access_as: { agent: {} } }) }
+ let!(:impersonate_authorization) { create(:agent_project_authorization, project: project, config: { access_as: { impersonate: {} } }) }
+ let!(:ci_user_authorization) { create(:agent_project_authorization, project: project, config: { access_as: { ci_user: {} } }) }
+ let!(:ci_job_authorization) { create(:agent_project_authorization, project: project, config: { access_as: { ci_job: {} } }) }
+ let!(:unexpected_authorization) { create(:agent_project_authorization, project: project, config: { access_as: { unexpected: {} } }) }
+
+ subject { Clusters::Agents::ProjectAuthorization.with_available_ci_access_fields(project) }
+
+ it { is_expected.to contain_exactly(agent_authorization_0, agent_authorization_1, agent_authorization_2) }
+ end
+end
diff --git a/spec/models/concerns/database_reflection_spec.rb b/spec/models/concerns/database_reflection_spec.rb
new file mode 100644
index 00000000000..4111f29ea8d
--- /dev/null
+++ b/spec/models/concerns/database_reflection_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe DatabaseReflection do
+ describe '.reflect' do
+ it 'returns a Reflection instance' do
+ expect(User.database).to be_an_instance_of(Gitlab::Database::Reflection)
+ end
+
+ it 'memoizes the result' do
+ instance1 = User.database
+ instance2 = User.database
+
+ expect(instance1).to equal(instance2)
+ end
+ end
+end
diff --git a/spec/models/concerns/has_integrations_spec.rb b/spec/models/concerns/has_integrations_spec.rb
deleted file mode 100644
index ea6b0e69209..00000000000
--- a/spec/models/concerns/has_integrations_spec.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe HasIntegrations do
- let_it_be(:project_1) { create(:project) }
- let_it_be(:project_2) { create(:project) }
- let_it_be(:project_3) { create(:project) }
- let_it_be(:project_4) { create(:project) }
- let_it_be(:instance_integration) { create(:jira_integration, :instance) }
-
- before do
- create(:jira_integration, project: project_1, inherit_from_id: instance_integration.id)
- create(:jira_integration, project: project_2, inherit_from_id: nil)
- create(:jira_integration, group: create(:group), project: nil, inherit_from_id: nil)
- create(:jira_integration, project: project_3, inherit_from_id: nil)
- create(:integrations_slack, project: project_4, inherit_from_id: nil)
- end
-
- describe '.without_integration' do
- it 'returns projects without integration' do
- expect(Project.without_integration(instance_integration)).to contain_exactly(project_4)
- end
- end
-end
diff --git a/spec/models/concerns/legacy_bulk_insert_spec.rb b/spec/models/concerns/legacy_bulk_insert_spec.rb
new file mode 100644
index 00000000000..0c6f84f391b
--- /dev/null
+++ b/spec/models/concerns/legacy_bulk_insert_spec.rb
@@ -0,0 +1,103 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# rubocop: disable Gitlab/BulkInsert
+RSpec.describe LegacyBulkInsert do
+ let(:model) { ApplicationRecord }
+
+ describe '#bulk_insert' do
+ before do
+ allow(model).to receive(:connection).and_return(dummy_connection)
+ allow(dummy_connection).to receive(:quote_column_name, &:itself)
+ allow(dummy_connection).to receive(:quote, &:itself)
+ allow(dummy_connection).to receive(:execute)
+ end
+
+ let(:dummy_connection) { double(:connection) }
+
+ let(:rows) do
+ [
+ { a: 1, b: 2, c: 3 },
+ { c: 6, a: 4, b: 5 }
+ ]
+ end
+
+ it 'does nothing with empty rows' do
+ expect(dummy_connection).not_to receive(:execute)
+
+ model.legacy_bulk_insert('test', [])
+ end
+
+ it 'uses the ordering from the first row' do
+ expect(dummy_connection).to receive(:execute) do |sql|
+ expect(sql).to include('(1, 2, 3)')
+ expect(sql).to include('(4, 5, 6)')
+ end
+
+ model.legacy_bulk_insert('test', rows)
+ end
+
+ it 'quotes column names' do
+ expect(dummy_connection).to receive(:quote_column_name).with(:a)
+ expect(dummy_connection).to receive(:quote_column_name).with(:b)
+ expect(dummy_connection).to receive(:quote_column_name).with(:c)
+
+ model.legacy_bulk_insert('test', rows)
+ end
+
+ it 'quotes values' do
+ 1.upto(6) do |i|
+ expect(dummy_connection).to receive(:quote).with(i)
+ end
+
+ model.legacy_bulk_insert('test', rows)
+ end
+
+ it 'does not quote values of a column in the disable_quote option' do
+ [1, 2, 4, 5].each do |i|
+ expect(dummy_connection).to receive(:quote).with(i)
+ end
+
+ model.legacy_bulk_insert('test', rows, disable_quote: :c)
+ end
+
+ it 'does not quote values of columns in the disable_quote option' do
+ [2, 5].each do |i|
+ expect(dummy_connection).to receive(:quote).with(i)
+ end
+
+ model.legacy_bulk_insert('test', rows, disable_quote: [:a, :c])
+ end
+
+ it 'handles non-UTF-8 data' do
+ expect { model.legacy_bulk_insert('test', [{ a: "\255" }]) }.not_to raise_error
+ end
+
+ context 'when using PostgreSQL' do
+ it 'allows the returning of the IDs of the inserted rows' do
+ result = double(:result, values: [['10']])
+
+ expect(dummy_connection)
+ .to receive(:execute)
+ .with(/RETURNING id/)
+ .and_return(result)
+
+ ids = model
+ .legacy_bulk_insert('test', [{ number: 10 }], return_ids: true)
+
+ expect(ids).to eq([10])
+ end
+
+ it 'allows setting the upsert to do nothing' do
+ expect(dummy_connection)
+ .to receive(:execute)
+ .with(/ON CONFLICT DO NOTHING/)
+
+ model
+ .legacy_bulk_insert('test', [{ number: 10 }], on_conflict: :do_nothing)
+ end
+ end
+ end
+end
+# rubocop: enable Gitlab/BulkInsert
diff --git a/spec/models/concerns/loaded_in_group_list_spec.rb b/spec/models/concerns/loaded_in_group_list_spec.rb
index c37943022ba..d38e842c666 100644
--- a/spec/models/concerns/loaded_in_group_list_spec.rb
+++ b/spec/models/concerns/loaded_in_group_list_spec.rb
@@ -3,49 +3,67 @@
require 'spec_helper'
RSpec.describe LoadedInGroupList do
- let(:parent) { create(:group) }
+ let_it_be(:parent) { create(:group) }
+ let_it_be(:group) { create(:group, parent: parent) }
+ let_it_be(:project) { create(:project, namespace: parent) }
- subject(:found_group) { Group.with_selects_for_list.find_by(id: parent.id) }
+ let(:archived_parameter) { nil }
- describe '.with_selects_for_list' do
- it 'includes the preloaded counts for groups' do
- create(:group, parent: parent)
- create(:project, namespace: parent)
- parent.add_developer(create(:user))
+ before do
+ parent.add_developer(create(:user))
+ end
- found_group = Group.with_selects_for_list.find_by(id: parent.id)
+ subject(:found_group) { Group.with_selects_for_list(archived: archived_parameter).find_by(id: parent.id) }
+ describe '.with_selects_for_list' do
+ it 'includes the preloaded counts for groups' do
expect(found_group.preloaded_project_count).to eq(1)
expect(found_group.preloaded_subgroup_count).to eq(1)
expect(found_group.preloaded_member_count).to eq(1)
end
+ context 'with project namespaces' do
+ let_it_be(:group1) { create(:group, parent: parent) }
+ let_it_be(:group2) { create(:group, parent: parent) }
+ let_it_be(:project_namespace) { project.project_namespace }
+
+ it 'does not include project_namespaces in the count of subgroups' do
+ expect(found_group.preloaded_subgroup_count).to eq(3)
+ expect(parent.subgroup_count).to eq(3)
+ end
+ end
+
context 'with archived projects' do
- it 'counts including archived projects when `true` is passed' do
- create(:project, namespace: parent, archived: true)
- create(:project, namespace: parent)
+ let_it_be(:archived_project) { create(:project, namespace: parent, archived: true) }
- found_group = Group.with_selects_for_list(archived: 'true').find_by(id: parent.id)
+ let(:archived_parameter) { true }
+ it 'counts including archived projects when `true` is passed' do
expect(found_group.preloaded_project_count).to eq(2)
end
- it 'counts only archived projects when `only` is passed' do
- create_list(:project, 2, namespace: parent, archived: true)
- create(:project, namespace: parent)
+ context 'when not counting archived projects' do
+ let(:archived_parameter) { false }
+
+ it 'counts projects without archived ones' do
+ expect(found_group.preloaded_project_count).to eq(1)
+ end
+ end
+
+ context 'with archived only' do
+ let_it_be(:archived_project2) { create(:project, namespace: parent, archived: true) }
- found_group = Group.with_selects_for_list(archived: 'only').find_by(id: parent.id)
+ let(:archived_parameter) { 'only' }
- expect(found_group.preloaded_project_count).to eq(2)
+ it 'counts only archived projects when `only` is passed' do
+ expect(found_group.preloaded_project_count).to eq(2)
+ end
end
end
end
describe '#children_count' do
it 'counts groups and projects' do
- create(:group, parent: parent)
- create(:project, namespace: parent)
-
expect(found_group.children_count).to eq(2)
end
end
diff --git a/spec/models/concerns/loose_foreign_key_spec.rb b/spec/models/concerns/loose_foreign_key_spec.rb
index ce5e33261a9..42da69eb75e 100644
--- a/spec/models/concerns/loose_foreign_key_spec.rb
+++ b/spec/models/concerns/loose_foreign_key_spec.rb
@@ -9,8 +9,8 @@ RSpec.describe LooseForeignKey do
self.table_name = 'projects'
- loose_foreign_key :issues, :project_id, on_delete: :async_delete, gitlab_schema: :gitlab_main
- loose_foreign_key 'merge_requests', 'project_id', 'on_delete' => 'async_nullify', 'gitlab_schema' => :gitlab_main
+ loose_foreign_key :issues, :project_id, on_delete: :async_delete
+ loose_foreign_key 'merge_requests', 'project_id', 'on_delete' => 'async_nullify'
end
end
@@ -28,7 +28,6 @@ RSpec.describe LooseForeignKey do
expect(definition.to_table).to eq('merge_requests')
expect(definition.column).to eq('project_id')
expect(definition.on_delete).to eq(:async_nullify)
- expect(definition.options[:gitlab_schema]).to eq(:gitlab_main)
end
context 'validation' do
@@ -39,9 +38,9 @@ RSpec.describe LooseForeignKey do
self.table_name = 'projects'
- loose_foreign_key :issues, :project_id, on_delete: :async_delete, gitlab_schema: :gitlab_main
- loose_foreign_key :merge_requests, :project_id, on_delete: :async_nullify, gitlab_schema: :gitlab_main
- loose_foreign_key :merge_requests, :project_id, on_delete: :destroy, gitlab_schema: :gitlab_main
+ loose_foreign_key :issues, :project_id, on_delete: :async_delete
+ loose_foreign_key :merge_requests, :project_id, on_delete: :async_nullify
+ loose_foreign_key :merge_requests, :project_id, on_delete: :destroy
end
end
@@ -50,28 +49,12 @@ RSpec.describe LooseForeignKey do
end
end
- context 'gitlab_schema validation' do
- let(:invalid_class) do
- Class.new(ApplicationRecord) do
- include LooseForeignKey
-
- self.table_name = 'projects'
-
- loose_foreign_key :merge_requests, :project_id, on_delete: :async_nullify, gitlab_schema: :unknown
- end
- end
-
- it 'raises error when invalid `gitlab_schema` option was given' do
- expect { invalid_class }.to raise_error /Invalid gitlab_schema option given: unknown/
- end
- end
-
context 'inheritance validation' do
let(:inherited_project_class) do
Class.new(Project) do
include LooseForeignKey
- loose_foreign_key :issues, :project_id, on_delete: :async_delete, gitlab_schema: :gitlab_main
+ loose_foreign_key :issues, :project_id, on_delete: :async_delete
end
end
diff --git a/spec/models/concerns/noteable_spec.rb b/spec/models/concerns/noteable_spec.rb
index 38766d8decd..81ae30b7116 100644
--- a/spec/models/concerns/noteable_spec.rb
+++ b/spec/models/concerns/noteable_spec.rb
@@ -77,6 +77,70 @@ RSpec.describe Noteable do
end
end
+ describe '#discussion_root_note_ids' do
+ let!(:label_event) { create(:resource_label_event, merge_request: subject) }
+ let!(:system_note) { create(:system_note, project: project, noteable: subject) }
+ let!(:milestone_event) { create(:resource_milestone_event, merge_request: subject) }
+ let!(:state_event) { create(:resource_state_event, merge_request: subject) }
+
+ it 'returns ordered discussion_ids and synthetic note ids' do
+ discussions = subject.discussion_root_note_ids(notes_filter: UserPreference::NOTES_FILTERS[:all_notes]).map do |n|
+ { table_name: n.table_name, discussion_id: n.discussion_id, id: n.id }
+ end
+
+ expect(discussions).to match([
+ a_hash_including(table_name: 'notes', discussion_id: active_diff_note1.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: active_diff_note3.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: outdated_diff_note1.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: discussion_note1.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: commit_diff_note1.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: commit_note1.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: commit_note2.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: commit_discussion_note1.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: commit_discussion_note3.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: note1.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: note2.discussion_id),
+ a_hash_including(table_name: 'resource_label_events', id: label_event.id),
+ a_hash_including(table_name: 'notes', discussion_id: system_note.discussion_id),
+ a_hash_including(table_name: 'resource_milestone_events', id: milestone_event.id),
+ a_hash_including(table_name: 'resource_state_events', id: state_event.id)
+ ])
+ end
+
+ it 'filters by comments only' do
+ discussions = subject.discussion_root_note_ids(notes_filter: UserPreference::NOTES_FILTERS[:only_comments]).map do |n|
+ { table_name: n.table_name, discussion_id: n.discussion_id, id: n.id }
+ end
+
+ expect(discussions).to match([
+ a_hash_including(table_name: 'notes', discussion_id: active_diff_note1.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: active_diff_note3.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: outdated_diff_note1.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: discussion_note1.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: commit_diff_note1.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: commit_note1.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: commit_note2.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: commit_discussion_note1.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: commit_discussion_note3.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: note1.discussion_id),
+ a_hash_including(table_name: 'notes', discussion_id: note2.discussion_id)
+ ])
+ end
+
+ it 'filters by system notes only' do
+ discussions = subject.discussion_root_note_ids(notes_filter: UserPreference::NOTES_FILTERS[:only_activity]).map do |n|
+ { table_name: n.table_name, discussion_id: n.discussion_id, id: n.id }
+ end
+
+ expect(discussions).to match([
+ a_hash_including(table_name: 'resource_label_events', id: label_event.id),
+ a_hash_including(table_name: 'notes', discussion_id: system_note.discussion_id),
+ a_hash_including(table_name: 'resource_milestone_events', id: milestone_event.id),
+ a_hash_including(table_name: 'resource_state_events', id: state_event.id)
+ ])
+ end
+ end
+
describe '#grouped_diff_discussions' do
let(:grouped_diff_discussions) { subject.grouped_diff_discussions }
diff --git a/spec/models/concerns/prometheus_adapter_spec.rb b/spec/models/concerns/prometheus_adapter_spec.rb
index 01c987a1d92..4158e8a0a4c 100644
--- a/spec/models/concerns/prometheus_adapter_spec.rb
+++ b/spec/models/concerns/prometheus_adapter_spec.rb
@@ -165,6 +165,14 @@ RSpec.describe PrometheusAdapter, :use_clean_rails_memory_store_caching do
it { is_expected.to eq(success: false, result: %(#{status} - "QUERY FAILED!")) }
end
end
+
+ context "when client raises Gitlab::PrometheusClient::ConnectionError" do
+ before do
+ stub_any_prometheus_request.to_raise(Gitlab::PrometheusClient::ConnectionError)
+ end
+
+ it { is_expected.to include(success: false, result: kind_of(String)) }
+ end
end
describe '#build_query_args' do
diff --git a/spec/models/concerns/reactive_caching_spec.rb b/spec/models/concerns/reactive_caching_spec.rb
index 7e031bdd263..4f3b95e43cd 100644
--- a/spec/models/concerns/reactive_caching_spec.rb
+++ b/spec/models/concerns/reactive_caching_spec.rb
@@ -375,7 +375,7 @@ RSpec.describe ReactiveCaching, :use_clean_rails_memory_store_caching do
end
describe 'classes including this concern' do
- it 'sets reactive_cache_work_type' do
+ it 'sets reactive_cache_work_type', :eager_load do
classes = ObjectSpace.each_object(Class).select do |klass|
klass < described_class && klass.name
end
diff --git a/spec/models/concerns/sha256_attribute_spec.rb b/spec/models/concerns/sha256_attribute_spec.rb
index c247865d77f..02947325bf4 100644
--- a/spec/models/concerns/sha256_attribute_spec.rb
+++ b/spec/models/concerns/sha256_attribute_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Sha256Attribute do
- let(:model) { Class.new { include Sha256Attribute } }
+ let(:model) { Class.new(ApplicationRecord) { include Sha256Attribute } }
before do
columns = [
diff --git a/spec/models/concerns/sha_attribute_spec.rb b/spec/models/concerns/sha_attribute_spec.rb
index 3846dd9c231..220eadfab92 100644
--- a/spec/models/concerns/sha_attribute_spec.rb
+++ b/spec/models/concerns/sha_attribute_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe ShaAttribute do
- let(:model) { Class.new { include ShaAttribute } }
+ let(:model) { Class.new(ApplicationRecord) { include ShaAttribute } }
before do
columns = [
diff --git a/spec/models/concerns/where_composite_spec.rb b/spec/models/concerns/where_composite_spec.rb
index 5e67f2f5b65..6abdd12aac5 100644
--- a/spec/models/concerns/where_composite_spec.rb
+++ b/spec/models/concerns/where_composite_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe WhereComposite do
describe '.where_composite' do
- let_it_be(:test_table_name) { "test_table_#{SecureRandom.hex(10)}" }
+ let_it_be(:test_table_name) { "_test_table_#{SecureRandom.hex(10)}" }
let(:model) do
tbl_name = test_table_name
diff --git a/spec/models/concerns/x509_serial_number_attribute_spec.rb b/spec/models/concerns/x509_serial_number_attribute_spec.rb
index 88550823748..723e2ad07b6 100644
--- a/spec/models/concerns/x509_serial_number_attribute_spec.rb
+++ b/spec/models/concerns/x509_serial_number_attribute_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe X509SerialNumberAttribute do
- let(:model) { Class.new { include X509SerialNumberAttribute } }
+ let(:model) { Class.new(ApplicationRecord) { include X509SerialNumberAttribute } }
before do
columns = [
diff --git a/spec/models/custom_emoji_spec.rb b/spec/models/custom_emoji_spec.rb
index 4a8b671bab7..01252a58681 100644
--- a/spec/models/custom_emoji_spec.rb
+++ b/spec/models/custom_emoji_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe CustomEmoji do
end
describe 'exclusion of duplicated emoji' do
- let(:emoji_name) { Gitlab::Emoji.emojis_names.sample }
+ let(:emoji_name) { TanukiEmoji.index.all.sample.name }
let(:group) { create(:group, :private) }
it 'disallows emoji names of built-in emoji' do
diff --git a/spec/models/customer_relations/contact_spec.rb b/spec/models/customer_relations/contact_spec.rb
index 298d5db3ab9..3a2d4e2d0ca 100644
--- a/spec/models/customer_relations/contact_spec.rb
+++ b/spec/models/customer_relations/contact_spec.rb
@@ -6,7 +6,8 @@ RSpec.describe CustomerRelations::Contact, type: :model do
describe 'associations' do
it { is_expected.to belong_to(:group) }
it { is_expected.to belong_to(:organization).optional }
- it { is_expected.to have_and_belong_to_many(:issues) }
+ it { is_expected.to have_many(:issue_contacts) }
+ it { is_expected.to have_many(:issues) }
end
describe 'validations' do
diff --git a/spec/models/customer_relations/issue_contact_spec.rb b/spec/models/customer_relations/issue_contact_spec.rb
new file mode 100644
index 00000000000..3747d159833
--- /dev/null
+++ b/spec/models/customer_relations/issue_contact_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe CustomerRelations::IssueContact do
+ let_it_be(:issue_contact, reload: true) { create(:issue_customer_relations_contact) }
+
+ subject { issue_contact }
+
+ it { expect(subject).to be_valid }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:issue).required }
+ it { is_expected.to belong_to(:contact).required }
+ end
+
+ describe 'factory' do
+ let(:built) { build(:issue_customer_relations_contact) }
+ let(:stubbed) { build_stubbed(:issue_customer_relations_contact) }
+ let(:created) { create(:issue_customer_relations_contact) }
+
+ let(:group) { build(:group) }
+ let(:project) { build(:project, group: group) }
+ let(:issue) { build(:issue, project: project) }
+ let(:contact) { build(:contact, group: group) }
+ let(:for_issue) { build(:issue_customer_relations_contact, :for_issue, issue: issue) }
+ let(:for_contact) { build(:issue_customer_relations_contact, :for_contact, contact: contact) }
+
+ it 'uses objects from the same group', :aggregate_failures do
+ expect(stubbed.contact.group).to eq(stubbed.issue.project.group)
+ expect(built.contact.group).to eq(built.issue.project.group)
+ expect(created.contact.group).to eq(created.issue.project.group)
+ end
+
+ it 'builds using the same group', :aggregate_failures do
+ expect(for_issue.contact.group).to eq(group)
+ expect(for_contact.issue.project.group).to eq(group)
+ end
+ end
+
+ describe 'validation' do
+ let(:built) { build(:issue_customer_relations_contact, issue: create(:issue), contact: create(:contact)) }
+
+ it 'fails when the contact group does not match the issue group' do
+ expect(built).not_to be_valid
+ end
+ end
+end
diff --git a/spec/models/data_list_spec.rb b/spec/models/data_list_spec.rb
new file mode 100644
index 00000000000..d2f15386808
--- /dev/null
+++ b/spec/models/data_list_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe DataList do
+ describe '#to_array' do
+ let(:jira_integration) { create(:jira_integration) }
+ let(:zentao_integration) { create(:zentao_integration) }
+ let(:cases) do
+ [
+ [jira_integration, 'Integrations::JiraTrackerData', 'service_id'],
+ [zentao_integration, 'Integrations::ZentaoTrackerData', 'integration_id']
+ ]
+ end
+
+ def data_list(integration)
+ DataList.new([integration], integration.to_data_fields_hash, integration.data_fields.class).to_array
+ end
+
+ it 'returns current data' do
+ cases.each do |integration, data_fields_class_name, foreign_key|
+ data_fields_klass, columns, values_items = data_list(integration)
+
+ expect(data_fields_klass.to_s).to eq data_fields_class_name
+ expect(columns.last).to eq foreign_key
+ values = values_items.first
+ expect(values.last).to eq integration.id
+ end
+ end
+ end
+end
diff --git a/spec/models/dependency_proxy/manifest_spec.rb b/spec/models/dependency_proxy/manifest_spec.rb
index e7f0889345a..59415096989 100644
--- a/spec/models/dependency_proxy/manifest_spec.rb
+++ b/spec/models/dependency_proxy/manifest_spec.rb
@@ -15,6 +15,17 @@ RSpec.describe DependencyProxy::Manifest, type: :model do
it { is_expected.to validate_presence_of(:digest) }
end
+ describe 'scopes' do
+ let_it_be(:manifest_one) { create(:dependency_proxy_manifest) }
+ let_it_be(:manifest_two) { create(:dependency_proxy_manifest) }
+ let_it_be(:manifests) { [manifest_one, manifest_two] }
+ let_it_be(:ids) { manifests.map(&:id) }
+
+ it 'order_id_desc' do
+ expect(described_class.where(id: ids).order_id_desc.to_a).to eq [manifest_two, manifest_one]
+ end
+ end
+
describe 'file is being stored' do
subject { create(:dependency_proxy_manifest) }
@@ -31,18 +42,14 @@ RSpec.describe DependencyProxy::Manifest, type: :model do
end
end
- describe '.find_or_initialize_by_file_name_or_digest' do
+ describe '.find_by_file_name_or_digest' do
let_it_be(:file_name) { 'foo' }
let_it_be(:digest) { 'bar' }
- subject { DependencyProxy::Manifest.find_or_initialize_by_file_name_or_digest(file_name: file_name, digest: digest) }
+ subject { DependencyProxy::Manifest.find_by_file_name_or_digest(file_name: file_name, digest: digest) }
context 'no manifest exists' do
- it 'initializes a manifest' do
- expect(DependencyProxy::Manifest).to receive(:new).with(file_name: file_name, digest: digest)
-
- subject
- end
+ it { is_expected.to be_nil }
end
context 'manifest exists and matches file_name' do
diff --git a/spec/models/deploy_key_spec.rb b/spec/models/deploy_key_spec.rb
index fa78527e366..c22bad0e062 100644
--- a/spec/models/deploy_key_spec.rb
+++ b/spec/models/deploy_key_spec.rb
@@ -5,6 +5,17 @@ require 'spec_helper'
RSpec.describe DeployKey, :mailer do
describe "Associations" do
it { is_expected.to have_many(:deploy_keys_projects) }
+ it do
+ is_expected.to have_many(:deploy_keys_projects_with_write_access)
+ .conditions(can_push: true)
+ .class_name('DeployKeysProject')
+ end
+ it do
+ is_expected.to have_many(:projects_with_write_access)
+ .class_name('Project')
+ .through(:deploy_keys_projects_with_write_access)
+ .source(:project)
+ end
it { is_expected.to have_many(:projects) }
it { is_expected.to have_many(:protected_branch_push_access_levels) }
end
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index f9a05fbb06f..51e1e63da8d 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -385,6 +385,43 @@ RSpec.describe Deployment do
end
end
+ describe '.archivables_in' do
+ subject { described_class.archivables_in(project, limit: limit) }
+
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:deployment_1) { create(:deployment, project: project) }
+ let_it_be(:deployment_2) { create(:deployment, project: project) }
+ let_it_be(:deployment_3) { create(:deployment, project: project) }
+
+ let(:limit) { 100 }
+
+ context 'when there are no archivable deployments in the project' do
+ it 'returns nothing' do
+ expect(subject).to be_empty
+ end
+ end
+
+ context 'when there are archivable deployments in the project' do
+ before do
+ stub_const("::Deployment::ARCHIVABLE_OFFSET", 1)
+ end
+
+ it 'returns all archivable deployments' do
+ expect(subject.count).to eq(2)
+ expect(subject).to contain_exactly(deployment_1, deployment_2)
+ end
+
+ context 'with limit' do
+ let(:limit) { 1 }
+
+ it 'takes the limit into account' do
+ expect(subject.count).to eq(1)
+ expect(subject.take).to be_in([deployment_1, deployment_2])
+ end
+ end
+ end
+ end
+
describe 'scopes' do
describe 'last_for_environment' do
let(:production) { create(:environment) }
@@ -456,6 +493,17 @@ RSpec.describe Deployment do
end
end
+ describe '.ordered' do
+ let!(:deployment1) { create(:deployment, status: :running) }
+ let!(:deployment2) { create(:deployment, status: :success, finished_at: Time.current) }
+ let!(:deployment3) { create(:deployment, status: :canceled, finished_at: 1.day.ago) }
+ let!(:deployment4) { create(:deployment, status: :success, finished_at: 2.days.ago) }
+
+ it 'sorts by finished at' do
+ expect(described_class.ordered).to eq([deployment1, deployment2, deployment3, deployment4])
+ end
+ end
+
describe 'visible' do
subject { described_class.visible }
@@ -763,6 +811,7 @@ RSpec.describe Deployment do
it 'schedules workers when finishing a deploy' do
expect(Deployments::UpdateEnvironmentWorker).to receive(:perform_async)
expect(Deployments::LinkMergeRequestWorker).to receive(:perform_async)
+ expect(Deployments::ArchiveInProjectWorker).to receive(:perform_async)
expect(Deployments::HooksWorker).to receive(:perform_async)
expect(deploy.update_status('success')).to eq(true)
@@ -840,6 +889,12 @@ RSpec.describe Deployment do
context 'with created deployment' do
let(:deployment_status) { :created }
+ context 'with created build' do
+ let(:build_status) { :created }
+
+ it_behaves_like 'ignoring build'
+ end
+
context 'with running build' do
let(:build_status) { :running }
@@ -862,12 +917,16 @@ RSpec.describe Deployment do
context 'with running deployment' do
let(:deployment_status) { :running }
+ context 'with created build' do
+ let(:build_status) { :created }
+
+ it_behaves_like 'ignoring build'
+ end
+
context 'with running build' do
let(:build_status) { :running }
- it_behaves_like 'gracefully handling error' do
- let(:error_message) { %Q{Status cannot transition via \"run\"} }
- end
+ it_behaves_like 'ignoring build'
end
context 'with finished build' do
@@ -886,6 +945,12 @@ RSpec.describe Deployment do
context 'with finished deployment' do
let(:deployment_status) { :success }
+ context 'with created build' do
+ let(:build_status) { :created }
+
+ it_behaves_like 'ignoring build'
+ end
+
context 'with running build' do
let(:build_status) { :running }
@@ -897,9 +962,13 @@ RSpec.describe Deployment do
context 'with finished build' do
let(:build_status) { :success }
- it_behaves_like 'gracefully handling error' do
- let(:error_message) { %Q{Status cannot transition via \"succeed\"} }
- end
+ it_behaves_like 'ignoring build'
+ end
+
+ context 'with failed build' do
+ let(:build_status) { :failed }
+
+ it_behaves_like 'synchronizing deployment'
end
context 'with unrelated build' do
diff --git a/spec/models/design_management/version_spec.rb b/spec/models/design_management/version_spec.rb
index e004ad024bc..303bac61e1e 100644
--- a/spec/models/design_management/version_spec.rb
+++ b/spec/models/design_management/version_spec.rb
@@ -283,7 +283,7 @@ RSpec.describe DesignManagement::Version do
it 'retrieves author from the Commit if author_id is nil and version has been persisted' do
author = create(:user)
version = create(:design_version, :committed, author: author)
- author.destroy
+ author.destroy!
version.reload
commit = version.issue.project.design_repository.commit(version.sha)
commit_user = create(:user, email: commit.author_email, name: commit.author_name)
diff --git a/spec/models/email_spec.rb b/spec/models/email_spec.rb
index 2b09ee5c190..59299a507e4 100644
--- a/spec/models/email_spec.rb
+++ b/spec/models/email_spec.rb
@@ -13,6 +13,15 @@ RSpec.describe Email do
it_behaves_like 'an object with RFC3696 compliant email-formatted attributes', :email do
subject { build(:email) }
end
+
+ context 'when the email conflicts with the primary email of a different user' do
+ let(:user) { create(:user) }
+ let(:email) { build(:email, email: user.email) }
+
+ it 'is invalid' do
+ expect(email).to be_invalid
+ end
+ end
end
it 'normalize email value' do
@@ -33,7 +42,7 @@ RSpec.describe Email do
end
describe 'scopes' do
- let(:user) { create(:user) }
+ let(:user) { create(:user, :unconfirmed) }
it 'scopes confirmed emails' do
create(:email, :confirmed, user: user)
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 08c639957d3..9d9862aa3d3 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
it 'ensures environment tier when a new object is created' do
environment = build(:environment, name: 'gprd', tier: nil)
- expect { environment.save }.to change { environment.tier }.from(nil).to('production')
+ expect { environment.save! }.to change { environment.tier }.from(nil).to('production')
end
it 'ensures environment tier when an existing object is updated' do
@@ -418,7 +418,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
context 'not in the same branch' do
before do
- deployment.update(sha: project.commit('feature').id)
+ deployment.update!(sha: project.commit('feature').id)
end
it 'returns false' do
@@ -496,7 +496,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
context 'when no other actions' do
context 'environment is available' do
before do
- environment.update(state: :available)
+ environment.update!(state: :available)
end
it do
@@ -508,7 +508,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
context 'environment is already stopped' do
before do
- environment.update(state: :stopped)
+ environment.update!(state: :stopped)
end
it do
@@ -1502,7 +1502,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
deployment = create(:deployment, :success, environment: environment, project: project)
deployment.create_ref
- expect { environment.destroy }.to change { project.commit(deployment.ref_path) }.to(nil)
+ expect { environment.destroy! }.to change { project.commit(deployment.ref_path) }.to(nil)
end
end
@@ -1517,7 +1517,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
end
it 'returns the environments count grouped by state with zero value' do
- environment2.update(state: 'stopped')
+ environment2.update!(state: 'stopped')
expect(project.environments.count_by_state).to eq({ stopped: 3, available: 0 })
end
end
@@ -1710,4 +1710,36 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
subject
end
end
+
+ describe '#should_link_to_merge_requests?' do
+ subject { environment.should_link_to_merge_requests? }
+
+ context 'when environment is foldered' do
+ context 'when environment is production tier' do
+ let(:environment) { create(:environment, project: project, name: 'production/aws') }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when environment is development tier' do
+ let(:environment) { create(:environment, project: project, name: 'review/feature') }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ context 'when environment is unfoldered' do
+ context 'when environment is production tier' do
+ let(:environment) { create(:environment, project: project, name: 'production') }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when environment is development tier' do
+ let(:environment) { create(:environment, project: project, name: 'development') }
+
+ it { is_expected.to eq(true) }
+ end
+ end
+ end
end
diff --git a/spec/models/error_tracking/error_event_spec.rb b/spec/models/error_tracking/error_event_spec.rb
index 8e20eb25353..9cf5a405e74 100644
--- a/spec/models/error_tracking/error_event_spec.rb
+++ b/spec/models/error_tracking/error_event_spec.rb
@@ -11,7 +11,10 @@ RSpec.describe ErrorTracking::ErrorEvent, type: :model do
describe 'validations' do
it { is_expected.to validate_presence_of(:description) }
+ it { is_expected.to validate_length_of(:description).is_at_most(1024) }
it { is_expected.to validate_presence_of(:occurred_at) }
+ it { is_expected.to validate_length_of(:level).is_at_most(255) }
+ it { is_expected.to validate_length_of(:environment).is_at_most(255) }
end
describe '#stacktrace' do
@@ -37,6 +40,23 @@ RSpec.describe ErrorTracking::ErrorEvent, type: :model do
expect(event.stacktrace).to be_kind_of(Array)
expect(event.stacktrace.first).to eq(expected_entry)
end
+
+ context 'error context is missing' do
+ let(:event) { create(:error_tracking_error_event, :browser) }
+
+ it 'generates a stacktrace without context' do
+ expected_entry = {
+ 'lineNo' => 6395,
+ 'context' => [],
+ 'filename' => 'webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js',
+ 'function' => 'hydrate',
+ 'colNo' => 0
+ }
+
+ expect(event.stacktrace).to be_kind_of(Array)
+ expect(event.stacktrace.first).to eq(expected_entry)
+ end
+ end
end
describe '#to_sentry_error_event' do
diff --git a/spec/models/error_tracking/error_spec.rb b/spec/models/error_tracking/error_spec.rb
index 9b8a81c6372..363cd197f3e 100644
--- a/spec/models/error_tracking/error_spec.rb
+++ b/spec/models/error_tracking/error_spec.rb
@@ -12,8 +12,12 @@ RSpec.describe ErrorTracking::Error, type: :model do
describe 'validations' do
it { is_expected.to validate_presence_of(:name) }
+ it { is_expected.to validate_length_of(:name).is_at_most(255) }
it { is_expected.to validate_presence_of(:description) }
+ it { is_expected.to validate_length_of(:description).is_at_most(1024) }
it { is_expected.to validate_presence_of(:actor) }
+ it { is_expected.to validate_length_of(:actor).is_at_most(255) }
+ it { is_expected.to validate_length_of(:platform).is_at_most(255) }
end
describe '.report_error' do
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index 41510b7aa1c..ee27eaf1d0b 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe Event do
describe 'after_create :set_last_repository_updated_at' do
context 'with a push event' do
it 'updates the project last_repository_updated_at' do
- project.update(last_repository_updated_at: 1.year.ago)
+ project.update!(last_repository_updated_at: 1.year.ago)
create_push_event(project, project.owner)
@@ -44,7 +44,7 @@ RSpec.describe Event do
context 'without a push event' do
it 'does not update the project last_repository_updated_at' do
- project.update(last_repository_updated_at: 1.year.ago)
+ project.update!(last_repository_updated_at: 1.year.ago)
create(:closed_issue_event, project: project, author: project.owner)
@@ -58,7 +58,7 @@ RSpec.describe Event do
describe '#set_last_repository_updated_at' do
it 'only updates once every Event::REPOSITORY_UPDATED_AT_INTERVAL minutes' do
last_known_timestamp = (Event::REPOSITORY_UPDATED_AT_INTERVAL - 1.minute).ago
- project.update(last_repository_updated_at: last_known_timestamp)
+ project.update!(last_repository_updated_at: last_known_timestamp)
project.reload # a reload removes fractions of seconds
expect do
@@ -73,7 +73,7 @@ RSpec.describe Event do
it 'passes event to UserInteractedProject.track' do
expect(UserInteractedProject).to receive(:track).with(event)
- event.save
+ event.save!
end
end
end
@@ -824,7 +824,7 @@ RSpec.describe Event do
context 'when a project was updated less than 1 hour ago' do
it 'does not update the project' do
- project.update(last_activity_at: Time.current)
+ project.update!(last_activity_at: Time.current)
expect(project).not_to receive(:update_column)
.with(:last_activity_at, a_kind_of(Time))
@@ -835,7 +835,7 @@ RSpec.describe Event do
context 'when a project was updated more than 1 hour ago' do
it 'updates the project' do
- project.update(last_activity_at: 1.year.ago)
+ project.update!(last_activity_at: 1.year.ago)
create_push_event(project, project.owner)
diff --git a/spec/models/fork_network_spec.rb b/spec/models/fork_network_spec.rb
index c2ef1fdcb5f..f2ec0ccb4fd 100644
--- a/spec/models/fork_network_spec.rb
+++ b/spec/models/fork_network_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe ForkNetwork do
describe '#add_root_as_member' do
it 'adds the root project as a member when creating a new root network' do
project = create(:project)
- fork_network = described_class.create(root_project: project)
+ fork_network = described_class.create!(root_project: project)
expect(fork_network.projects).to include(project)
end
@@ -54,8 +54,8 @@ RSpec.describe ForkNetwork do
first_fork = fork_project(first_project)
second_fork = fork_project(second_project)
- first_project.destroy
- second_project.destroy
+ first_project.destroy!
+ second_project.destroy!
expect(first_fork.fork_network).not_to be_nil
expect(first_fork.fork_network.root_project).to be_nil
diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb
index 6fe5a1407a9..9d70019734b 100644
--- a/spec/models/generic_commit_status_spec.rb
+++ b/spec/models/generic_commit_status_spec.rb
@@ -133,7 +133,7 @@ RSpec.describe GenericCommitStatus do
before do
generic_commit_status.context = nil
generic_commit_status.stage = nil
- generic_commit_status.save
+ generic_commit_status.save!
end
describe '#context' do
diff --git a/spec/models/grafana_integration_spec.rb b/spec/models/grafana_integration_spec.rb
index 79f102919ac..bb822187e0c 100644
--- a/spec/models/grafana_integration_spec.rb
+++ b/spec/models/grafana_integration_spec.rb
@@ -60,7 +60,7 @@ RSpec.describe GrafanaIntegration do
context 'with grafana integration enabled' do
it 'returns nil' do
- grafana_integration.update(enabled: false)
+ grafana_integration.update!(enabled: false)
expect(grafana_integration.client).to be(nil)
end
@@ -81,8 +81,8 @@ RSpec.describe GrafanaIntegration do
end
it 'prevents overriding token value with its encrypted or masked version', :aggregate_failures do
- expect { grafana_integration.update(token: grafana_integration.encrypted_token) }.not_to change { grafana_integration.reload.send(:token) }
- expect { grafana_integration.update(token: grafana_integration.masked_token) }.not_to change { grafana_integration.reload.send(:token) }
+ expect { grafana_integration.update!(token: grafana_integration.encrypted_token) }.not_to change { grafana_integration.reload.send(:token) }
+ expect { grafana_integration.update!(token: grafana_integration.masked_token) }.not_to change { grafana_integration.reload.send(:token) }
end
end
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index e88abc21ef2..735aa4df2ba 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -37,6 +37,8 @@ RSpec.describe Group do
it { is_expected.to have_many(:daily_build_group_report_results).class_name('Ci::DailyBuildGroupReportResult') }
it { is_expected.to have_many(:group_callouts).class_name('Users::GroupCallout').with_foreign_key(:group_id) }
it { is_expected.to have_many(:bulk_import_exports).class_name('BulkImports::Export') }
+ it { is_expected.to have_many(:contacts).class_name('CustomerRelations::Contact') }
+ it { is_expected.to have_many(:organizations).class_name('CustomerRelations::Organization') }
describe '#members & #requesters' do
let(:requester) { create(:user) }
@@ -160,7 +162,7 @@ RSpec.describe Group do
context 'when sub group is deleted' do
it 'does not delete parent notification settings' do
expect do
- sub_group.destroy
+ sub_group.destroy!
end.to change { NotificationSetting.count }.by(-1)
end
end
@@ -404,7 +406,7 @@ RSpec.describe Group do
subject do
recorded_queries.record do
- group.update(parent: new_parent)
+ group.update!(parent: new_parent)
end
end
@@ -496,7 +498,7 @@ RSpec.describe Group do
let!(:group) { create(:group, parent: parent_group) }
before do
- parent_group.update(parent: new_grandparent)
+ parent_group.update!(parent: new_grandparent)
end
it 'updates traversal_ids for all descendants' do
@@ -563,6 +565,15 @@ RSpec.describe Group do
it { expect(group.ancestors.to_sql).not_to include 'traversal_ids <@' }
end
end
+
+ context 'when project namespace exists in the group' do
+ let!(:project) { create(:project, group: group) }
+ let!(:project_namespace) { project.project_namespace }
+
+ it 'filters out project namespace' do
+ expect(group.descendants.find_by_id(project_namespace.id)).to be_nil
+ end
+ end
end
end
@@ -571,8 +582,8 @@ RSpec.describe Group do
let(:instance_integration) { build(:jira_integration, :instance) }
before do
- create(:jira_integration, group: group, project: nil)
- create(:integrations_slack, group: another_group, project: nil)
+ create(:jira_integration, :group, group: group)
+ create(:integrations_slack, :group, group: another_group)
end
it 'returns groups without integration' do
@@ -718,6 +729,22 @@ RSpec.describe Group do
expect(group.group_members.developers.map(&:user)).to include(user)
expect(group.group_members.guests.map(&:user)).not_to include(user)
end
+
+ context 'when `tasks_to_be_done` and `tasks_project_id` are passed' do
+ let!(:project) { create(:project, group: group) }
+
+ before do
+ stub_experiments(invite_members_for_task: true)
+ group.add_users([create(:user)], :developer, tasks_to_be_done: %w(ci code), tasks_project_id: project.id)
+ end
+
+ it 'creates a member_task with the correct attributes', :aggregate_failures do
+ member = group.group_members.last
+
+ expect(member.tasks_to_be_done).to match_array([:ci, :code])
+ expect(member.member_task.project).to eq(project)
+ end
+ end
end
describe '#avatar_type' do
@@ -831,7 +858,7 @@ RSpec.describe Group do
before do
parent_group = create(:group)
create(:group_member, :owner, group: parent_group)
- group.update(parent: parent_group)
+ group.update!(parent: parent_group)
end
it { expect(group.last_owner?(@members[:owner])).to be_falsy }
@@ -888,7 +915,7 @@ RSpec.describe Group do
before do
parent_group = create(:group)
create(:group_member, :owner, group: parent_group)
- group.update(parent: parent_group)
+ group.update!(parent: parent_group)
end
it { expect(group.member_last_blocked_owner?(member)).to be(false) }
@@ -1936,7 +1963,7 @@ RSpec.describe Group do
let(:environment) { 'foo%bar/test' }
it 'matches literally for %' do
- ci_variable.update(environment_scope: 'foo%bar/*')
+ ci_variable.update_attribute(:environment_scope, 'foo%bar/*')
is_expected.to contain_exactly(ci_variable)
end
@@ -2077,7 +2104,7 @@ RSpec.describe Group do
let(:ancestor_group) { create(:group) }
before do
- group.update(parent: ancestor_group)
+ group.update!(parent: ancestor_group)
end
it 'returns all ancestor group ids' do
@@ -2594,7 +2621,7 @@ RSpec.describe Group do
let_it_be(:project) { create(:project, group: group, service_desk_enabled: false) }
before do
- project.update(service_desk_enabled: false)
+ project.update!(service_desk_enabled: false)
end
it { is_expected.to eq(false) }
@@ -2623,14 +2650,6 @@ RSpec.describe Group do
end
it_behaves_like 'returns namespaces with disabled email'
-
- context 'when feature flag :linear_group_ancestor_scopes is disabled' do
- before do
- stub_feature_flags(linear_group_ancestor_scopes: false)
- end
-
- it_behaves_like 'returns namespaces with disabled email'
- end
end
describe '.timelogs' do
diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb
index d811f67d16b..f0ee9a613d8 100644
--- a/spec/models/hooks/project_hook_spec.rb
+++ b/spec/models/hooks/project_hook_spec.rb
@@ -32,8 +32,8 @@ RSpec.describe ProjectHook do
end
describe '#rate_limit' do
- let_it_be(:hook) { create(:project_hook) }
let_it_be(:plan_limits) { create(:plan_limits, :default_plan, web_hook_calls: 100) }
+ let_it_be(:hook) { create(:project_hook) }
it 'returns the default limit' do
expect(hook.rate_limit).to be(100)
diff --git a/spec/models/identity_spec.rb b/spec/models/identity_spec.rb
index c0efb2dff56..124c54a2028 100644
--- a/spec/models/identity_spec.rb
+++ b/spec/models/identity_spec.rb
@@ -118,19 +118,19 @@ RSpec.describe Identity do
it 'if extern_uid changes' do
expect(ldap_identity).not_to receive(:ensure_normalized_extern_uid)
- ldap_identity.save
+ ldap_identity.save!
end
it 'if current_uid is nil' do
expect(ldap_identity).to receive(:ensure_normalized_extern_uid)
- ldap_identity.update(extern_uid: nil)
+ ldap_identity.update!(extern_uid: nil)
expect(ldap_identity.extern_uid).to be_nil
end
it 'if extern_uid changed and not nil' do
- ldap_identity.update(extern_uid: 'uid=john1,ou=PEOPLE,dc=example,dc=com')
+ ldap_identity.update!(extern_uid: 'uid=john1,ou=PEOPLE,dc=example,dc=com')
expect(ldap_identity.extern_uid).to eq 'uid=john1,ou=people,dc=example,dc=com'
end
@@ -150,7 +150,7 @@ RSpec.describe Identity do
expect(user.user_synced_attributes_metadata.provider).to eq 'ldapmain'
- ldap_identity.destroy
+ ldap_identity.destroy!
expect(user.reload.user_synced_attributes_metadata).to be_nil
end
@@ -162,7 +162,7 @@ RSpec.describe Identity do
expect(user.user_synced_attributes_metadata.provider).to eq 'other'
- ldap_identity.destroy
+ ldap_identity.destroy!
expect(user.reload.user_synced_attributes_metadata.provider).to eq 'other'
end
diff --git a/spec/models/integration_spec.rb b/spec/models/integration_spec.rb
index 1a83d948fcf..de47fb3839a 100644
--- a/spec/models/integration_spec.rb
+++ b/spec/models/integration_spec.rb
@@ -299,7 +299,7 @@ RSpec.describe Integration do
end
context 'when integration is a group-level integration' do
- let(:group_integration) { create(:jira_integration, group: group, project: nil) }
+ let(:group_integration) { create(:jira_integration, :group, group: group) }
it 'sets inherit_from_id from integration' do
integration = described_class.build_from_integration(group_integration, project_id: project.id)
@@ -458,7 +458,7 @@ RSpec.describe Integration do
end
context 'with an active group-level integration' do
- let!(:group_integration) { create(:prometheus_integration, group: group, project: nil, api_url: 'https://prometheus.group.com/') }
+ let!(:group_integration) { create(:prometheus_integration, :group, group: group, api_url: 'https://prometheus.group.com/') }
it 'creates an integration from the group-level integration' do
described_class.create_from_active_default_integrations(project, :project_id)
@@ -481,7 +481,7 @@ RSpec.describe Integration do
end
context 'with an active subgroup' do
- let!(:subgroup_integration) { create(:prometheus_integration, group: subgroup, project: nil, api_url: 'https://prometheus.subgroup.com/') }
+ let!(:subgroup_integration) { create(:prometheus_integration, :group, group: subgroup, api_url: 'https://prometheus.subgroup.com/') }
let!(:subgroup) { create(:group, parent: group) }
let(:project) { create(:project, group: subgroup) }
@@ -509,7 +509,7 @@ RSpec.describe Integration do
end
context 'having an integration inheriting settings' do
- let!(:subgroup_integration) { create(:prometheus_integration, group: subgroup, project: nil, inherit_from_id: group_integration.id, api_url: 'https://prometheus.subgroup.com/') }
+ let!(:subgroup_integration) { create(:prometheus_integration, :group, group: subgroup, inherit_from_id: group_integration.id, api_url: 'https://prometheus.subgroup.com/') }
it 'creates an integration from the group-level integration' do
described_class.create_from_active_default_integrations(sub_subgroup, :group_id)
@@ -552,11 +552,11 @@ RSpec.describe Integration do
let_it_be(:subgroup2) { create(:group, parent: group) }
let_it_be(:project1) { create(:project, group: subgroup1) }
let_it_be(:project2) { create(:project, group: subgroup2) }
- let_it_be(:group_integration) { create(:prometheus_integration, group: group, project: nil) }
- let_it_be(:subgroup_integration1) { create(:prometheus_integration, group: subgroup1, project: nil, inherit_from_id: group_integration.id) }
- let_it_be(:subgroup_integration2) { create(:prometheus_integration, group: subgroup2, project: nil) }
- let_it_be(:project_integration1) { create(:prometheus_integration, group: nil, project: project1, inherit_from_id: group_integration.id) }
- let_it_be(:project_integration2) { create(:prometheus_integration, group: nil, project: project2, inherit_from_id: subgroup_integration2.id) }
+ let_it_be(:group_integration) { create(:prometheus_integration, :group, group: group) }
+ let_it_be(:subgroup_integration1) { create(:prometheus_integration, :group, group: subgroup1, inherit_from_id: group_integration.id) }
+ let_it_be(:subgroup_integration2) { create(:prometheus_integration, :group, group: subgroup2) }
+ let_it_be(:project_integration1) { create(:prometheus_integration, project: project1, inherit_from_id: group_integration.id) }
+ let_it_be(:project_integration2) { create(:prometheus_integration, project: project2, inherit_from_id: subgroup_integration2.id) }
it 'returns the groups and projects inheriting from integration ancestors', :aggregate_failures do
expect(described_class.inherited_descendants_from_self_or_ancestors_from(group_integration)).to eq([subgroup_integration1, project_integration1])
diff --git a/spec/models/integrations/jira_spec.rb b/spec/models/integrations/jira_spec.rb
index 0321b151633..1d81668f97d 100644
--- a/spec/models/integrations/jira_spec.rb
+++ b/spec/models/integrations/jira_spec.rb
@@ -495,6 +495,18 @@ RSpec.describe Integrations::Jira do
end
end
+ describe '#client' do
+ it 'uses the default GitLab::HTTP timeouts' do
+ timeouts = Gitlab::HTTP::DEFAULT_TIMEOUT_OPTIONS
+ stub_request(:get, 'http://jira.example.com/foo')
+
+ expect(Gitlab::HTTP).to receive(:httparty_perform_request)
+ .with(Net::HTTP::Get, '/foo', hash_including(timeouts)).and_call_original
+
+ jira_integration.client.get('/foo')
+ end
+ end
+
describe '#find_issue' do
let(:issue_key) { 'JIRA-123' }
let(:issue_url) { "#{url}/rest/api/2/issue/#{issue_key}" }
@@ -503,7 +515,7 @@ RSpec.describe Integrations::Jira do
stub_request(:get, issue_url).with(basic_auth: [username, password])
end
- it 'call the Jira API to get the issue' do
+ it 'calls the Jira API to get the issue' do
jira_integration.find_issue(issue_key)
expect(WebMock).to have_requested(:get, issue_url)
@@ -845,10 +857,14 @@ RSpec.describe Integrations::Jira do
let_it_be(:user) { build_stubbed(:user) }
let(:jira_issue) { ExternalIssue.new('JIRA-123', project) }
+ let(:success_message) { 'SUCCESS: Successfully posted to http://jira.example.com.' }
+ let(:favicon_path) { "http://localhost/assets/#{find_asset('favicon.png').digest_path}" }
subject { jira_integration.create_cross_reference_note(jira_issue, resource, user) }
- shared_examples 'creates a comment on Jira' do
+ shared_examples 'handles cross-references' do
+ let(:resource_name) { jira_integration.send(:noteable_name, resource) }
+ let(:resource_url) { jira_integration.send(:build_entity_url, resource_name, resource.to_param) }
let(:issue_url) { "#{url}/rest/api/2/issue/JIRA-123" }
let(:comment_url) { "#{issue_url}/comment" }
let(:remote_link_url) { "#{issue_url}/remotelink" }
@@ -860,12 +876,77 @@ RSpec.describe Integrations::Jira do
stub_request(:post, remote_link_url).with(basic_auth: [username, password])
end
- it 'creates a comment on Jira' do
- subject
+ context 'when enabled' do
+ before do
+ allow(jira_integration).to receive(:can_cross_reference?) { true }
+ end
- expect(WebMock).to have_requested(:post, comment_url).with(
- body: /mentioned this issue in/
- ).once
+ it 'creates a comment and remote link' do
+ expect(subject).to eq(success_message)
+ expect(WebMock).to have_requested(:post, comment_url).with(body: comment_body).once
+ expect(WebMock).to have_requested(:post, remote_link_url).with(
+ body: hash_including(
+ GlobalID: 'GitLab',
+ relationship: 'mentioned on',
+ object: {
+ url: resource_url,
+ title: "#{resource.model_name.human} - #{resource.title}",
+ icon: { title: 'GitLab', url16x16: favicon_path },
+ status: { resolved: false }
+ }
+ )
+ ).once
+ end
+
+ context 'when comment already exists' do
+ before do
+ allow(jira_integration).to receive(:comment_exists?) { true }
+ end
+
+ it 'does not create a comment or remote link' do
+ expect(subject).to be_nil
+ expect(WebMock).not_to have_requested(:post, comment_url)
+ expect(WebMock).not_to have_requested(:post, remote_link_url)
+ end
+ end
+
+ context 'when remote link already exists' do
+ let(:link) { double(object: { 'url' => resource_url }) }
+
+ before do
+ allow(jira_integration).to receive(:find_remote_link).and_return(link)
+ end
+
+ it 'updates the remote link but does not create a comment' do
+ expect(link).to receive(:save!)
+ expect(subject).to eq(success_message)
+ expect(WebMock).not_to have_requested(:post, comment_url)
+ end
+ end
+ end
+
+ context 'when disabled' do
+ before do
+ allow(jira_integration).to receive(:can_cross_reference?) { false }
+ end
+
+ it 'does not create a comment or remote link' do
+ expect(subject).to eq("Events for #{resource_name.pluralize.humanize(capitalize: false)} are disabled.")
+ expect(WebMock).not_to have_requested(:post, comment_url)
+ expect(WebMock).not_to have_requested(:post, remote_link_url)
+ end
+ end
+
+ context 'with jira_use_first_ref_by_oid feature flag disabled' do
+ before do
+ stub_feature_flags(jira_use_first_ref_by_oid: false)
+ end
+
+ it 'creates a comment and remote link on Jira' do
+ expect(subject).to eq(success_message)
+ expect(WebMock).to have_requested(:post, comment_url).with(body: comment_body).once
+ expect(WebMock).to have_requested(:post, remote_link_url).once
+ end
end
it 'tracks usage' do
@@ -877,39 +958,38 @@ RSpec.describe Integrations::Jira do
end
end
- context 'when resource is a commit' do
- let(:resource) { project.commit('master') }
-
- context 'when disabled' do
- before do
- allow_next_instance_of(described_class) do |instance|
- allow(instance).to receive(:commit_events) { false }
- end
- end
-
- it { is_expected.to eq('Events for commits are disabled.') }
+ context 'for commits' do
+ it_behaves_like 'handles cross-references' do
+ let(:resource) { project.commit('master') }
+ let(:comment_body) { /mentioned this issue in \[a commit\|.* on branch \[master\|/ }
end
+ end
- context 'when enabled' do
- it_behaves_like 'creates a comment on Jira'
+ context 'for issues' do
+ it_behaves_like 'handles cross-references' do
+ let(:resource) { build_stubbed(:issue, project: project) }
+ let(:comment_body) { /mentioned this issue in \[a issue\|/ }
end
end
- context 'when resource is a merge request' do
- let(:resource) { build_stubbed(:merge_request, source_project: project) }
-
- context 'when disabled' do
- before do
- allow_next_instance_of(described_class) do |instance|
- allow(instance).to receive(:merge_requests_events) { false }
- end
- end
+ context 'for merge requests' do
+ it_behaves_like 'handles cross-references' do
+ let(:resource) { build_stubbed(:merge_request, source_project: project) }
+ let(:comment_body) { /mentioned this issue in \[a merge request\|.* on branch \[master\|/ }
+ end
+ end
- it { is_expected.to eq('Events for merge requests are disabled.') }
+ context 'for notes' do
+ it_behaves_like 'handles cross-references' do
+ let(:resource) { build_stubbed(:note, project: project) }
+ let(:comment_body) { /mentioned this issue in \[a note\|/ }
end
+ end
- context 'when enabled' do
- it_behaves_like 'creates a comment on Jira'
+ context 'for snippets' do
+ it_behaves_like 'handles cross-references' do
+ let(:resource) { build_stubbed(:snippet, project: project) }
+ let(:comment_body) { /mentioned this issue in \[a snippet\|/ }
end
end
end
@@ -946,7 +1026,9 @@ RSpec.describe Integrations::Jira do
expect(jira_integration).to receive(:log_error).with(
'Error sending message',
client_url: 'http://jira.example.com',
- error: error_message
+ 'exception.class' => anything,
+ 'exception.message' => error_message,
+ 'exception.backtrace' => anything
)
expect(jira_integration.test(nil)).to eq(success: false, result: error_message)
diff --git a/spec/models/integrations/pipelines_email_spec.rb b/spec/models/integrations/pipelines_email_spec.rb
index afd9d71ebc4..d70f104b965 100644
--- a/spec/models/integrations/pipelines_email_spec.rb
+++ b/spec/models/integrations/pipelines_email_spec.rb
@@ -35,6 +35,42 @@ RSpec.describe Integrations::PipelinesEmail, :mailer do
it { is_expected.not_to validate_presence_of(:recipients) }
end
+
+ describe 'validates number of recipients' do
+ before do
+ stub_const("#{described_class}::RECIPIENTS_LIMIT", 2)
+ end
+
+ subject(:integration) { described_class.new(project: project, recipients: recipients, active: true) }
+
+ context 'valid number of recipients' do
+ let(:recipients) { 'foo@bar.com, , ' }
+
+ it 'does not count empty emails' do
+ is_expected.to be_valid
+ end
+ end
+
+ context 'invalid number of recipients' do
+ let(:recipients) { 'foo@bar.com bar@foo.com bob@gitlab.com' }
+
+ it { is_expected.not_to be_valid }
+
+ it 'adds an error message' do
+ integration.valid?
+
+ expect(integration.errors).to contain_exactly('Recipients can\'t exceed 2')
+ end
+
+ context 'when integration is not active' do
+ before do
+ integration.active = false
+ end
+
+ it { is_expected.to be_valid }
+ end
+ end
+ end
end
shared_examples 'sending email' do |branches_to_be_notified: nil|
@@ -50,7 +86,7 @@ RSpec.describe Integrations::PipelinesEmail, :mailer do
it 'sends email' do
emails = receivers.map { |r| double(notification_email_or_default: r) }
- should_only_email(*emails, kind: :bcc)
+ should_only_email(*emails)
end
end
diff --git a/spec/models/integrations/shimo_spec.rb b/spec/models/integrations/shimo_spec.rb
new file mode 100644
index 00000000000..25df8d2b249
--- /dev/null
+++ b/spec/models/integrations/shimo_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Integrations::Shimo do
+ describe '#fields' do
+ let(:shimo_integration) { create(:shimo_integration) }
+
+ it 'returns custom fields' do
+ expect(shimo_integration.fields.pluck(:name)).to eq(%w[external_wiki_url])
+ end
+ end
+
+ describe '#create' do
+ let(:project) { create(:project, :repository) }
+ let(:external_wiki_url) { 'https://shimo.example.com/desktop' }
+ let(:params) { { active: true, project: project, external_wiki_url: external_wiki_url } }
+
+ context 'with valid params' do
+ it 'creates the Shimo integration' do
+ shimo = described_class.create!(params)
+
+ expect(shimo.valid?).to be true
+ expect(shimo.render?).to be true
+ expect(shimo.external_wiki_url).to eq(external_wiki_url)
+ end
+ end
+
+ context 'with invalid params' do
+ it 'cannot create the Shimo integration without external_wiki_url' do
+ params['external_wiki_url'] = nil
+ expect { described_class.create!(params) }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+
+ it 'cannot create the Shimo integration with invalid external_wiki_url' do
+ params['external_wiki_url'] = 'Fake Invalid URL'
+ expect { described_class.create!(params) }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+ end
+ end
+end
diff --git a/spec/models/integrations/zentao_spec.rb b/spec/models/integrations/zentao_spec.rb
index a1503ecc092..2b0532c7930 100644
--- a/spec/models/integrations/zentao_spec.rb
+++ b/spec/models/integrations/zentao_spec.rb
@@ -50,4 +50,10 @@ RSpec.describe Integrations::Zentao do
expect(zentao_integration.test).to eq(test_response)
end
end
+
+ describe '#help' do
+ it 'renders prompt information' do
+ expect(zentao_integration.help).not_to be_empty
+ end
+ end
end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 4319407706e..ba4429451d1 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -34,7 +34,8 @@ RSpec.describe Issue do
it { is_expected.to have_many(:issue_email_participants) }
it { is_expected.to have_many(:timelogs).autosave(true) }
it { is_expected.to have_one(:incident_management_issuable_escalation_status) }
- it { is_expected.to have_and_belong_to_many(:customer_relations_contacts) }
+ it { is_expected.to have_many(:issue_customer_relations_contacts) }
+ it { is_expected.to have_many(:customer_relations_contacts) }
describe 'versions.most_recent' do
it 'returns the most recent version' do
@@ -305,7 +306,7 @@ RSpec.describe Issue do
end
describe '#reopen' do
- let(:issue) { create(:issue, project: reusable_project, state: 'closed', closed_at: Time.current, closed_by: user) }
+ let_it_be_with_reload(:issue) { create(:issue, project: reusable_project, state: 'closed', closed_at: Time.current, closed_by: user) }
it 'sets closed_at to nil when an issue is reopened' do
expect { issue.reopen }.to change { issue.closed_at }.to(nil)
@@ -315,6 +316,22 @@ RSpec.describe Issue do
expect { issue.reopen }.to change { issue.closed_by }.from(user).to(nil)
end
+ it 'clears moved_to_id for moved issues' do
+ moved_issue = create(:issue)
+
+ issue.update!(moved_to_id: moved_issue.id)
+
+ expect { issue.reopen }.to change { issue.moved_to_id }.from(moved_issue.id).to(nil)
+ end
+
+ it 'clears duplicated_to_id for duplicated issues' do
+ duplicate_issue = create(:issue)
+
+ issue.update!(duplicated_to_id: duplicate_issue.id)
+
+ expect { issue.reopen }.to change { issue.duplicated_to_id }.from(duplicate_issue.id).to(nil)
+ end
+
it 'changes the state to opened' do
expect { issue.reopen }.to change { issue.state_id }.from(described_class.available_states[:closed]).to(described_class.available_states[:opened])
end
@@ -1218,7 +1235,7 @@ RSpec.describe Issue do
end
it 'returns public and hidden issues' do
- expect(described_class.public_only).to eq([public_issue, hidden_issue])
+ expect(described_class.public_only).to contain_exactly(public_issue, hidden_issue)
end
end
end
@@ -1247,7 +1264,7 @@ RSpec.describe Issue do
end
it 'returns public and hidden issues' do
- expect(described_class.without_hidden).to eq([public_issue, hidden_issue])
+ expect(described_class.without_hidden).to contain_exactly(public_issue, hidden_issue)
end
end
end
diff --git a/spec/models/jira_import_state_spec.rb b/spec/models/jira_import_state_spec.rb
index e982b7353ba..a272d001429 100644
--- a/spec/models/jira_import_state_spec.rb
+++ b/spec/models/jira_import_state_spec.rb
@@ -175,7 +175,7 @@ RSpec.describe JiraImportState do
let(:jira_import) { build(:jira_import_state, project: project)}
it 'does not run the callback', :aggregate_failures do
- expect { jira_import.save }.to change { JiraImportState.count }.by(1)
+ expect { jira_import.save! }.to change { JiraImportState.count }.by(1)
expect(jira_import.reload.error_message).to be_nil
end
end
@@ -184,7 +184,7 @@ RSpec.describe JiraImportState do
let(:jira_import) { build(:jira_import_state, project: project, error_message: 'error')}
it 'does not run the callback', :aggregate_failures do
- expect { jira_import.save }.to change { JiraImportState.count }.by(1)
+ expect { jira_import.save! }.to change { JiraImportState.count }.by(1)
expect(jira_import.reload.error_message).to eq('error')
end
end
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index 7468c1b9f0a..d41a1604211 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -85,9 +85,9 @@ RSpec.describe Key, :mailer do
let_it_be(:expiring_soon_notified) { create(:key, expires_at: 4.days.from_now, user: user, before_expiry_notification_delivered_at: Time.current) }
let_it_be(:future_expiry) { create(:key, expires_at: 1.month.from_now, user: user) }
- describe '.expired_and_not_notified' do
+ describe '.expired_today_and_not_notified' do
it 'returns keys that expire today and in the past' do
- expect(described_class.expired_and_not_notified).to contain_exactly(expired_today_not_notified, expired_yesterday)
+ expect(described_class.expired_today_and_not_notified).to contain_exactly(expired_today_not_notified)
end
end
diff --git a/spec/models/loose_foreign_keys/deleted_record_spec.rb b/spec/models/loose_foreign_keys/deleted_record_spec.rb
new file mode 100644
index 00000000000..cd5068bdb52
--- /dev/null
+++ b/spec/models/loose_foreign_keys/deleted_record_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe LooseForeignKeys::DeletedRecord, type: :model do
+ let_it_be(:table) { 'public.projects' }
+
+ let_it_be(:deleted_record_1) { described_class.create!(partition: 1, fully_qualified_table_name: table, primary_key_value: 5) }
+ let_it_be(:deleted_record_2) { described_class.create!(partition: 1, fully_qualified_table_name: table, primary_key_value: 1) }
+ let_it_be(:deleted_record_3) { described_class.create!(partition: 1, fully_qualified_table_name: 'public.other_table', primary_key_value: 3) }
+ let_it_be(:deleted_record_4) { described_class.create!(partition: 1, fully_qualified_table_name: table, primary_key_value: 1) } # duplicate
+
+ describe '.load_batch_for_table' do
+ it 'loads records and orders them by creation date' do
+ records = described_class.load_batch_for_table(table, 10)
+
+ expect(records).to eq([deleted_record_1, deleted_record_2, deleted_record_4])
+ end
+
+ it 'supports configurable batch size' do
+ records = described_class.load_batch_for_table(table, 2)
+
+ expect(records).to eq([deleted_record_1, deleted_record_2])
+ end
+ end
+
+ describe '.mark_records_processed' do
+ it 'updates all records' do
+ described_class.mark_records_processed([deleted_record_1, deleted_record_2, deleted_record_4])
+
+ expect(described_class.status_pending.count).to eq(1)
+ expect(described_class.status_processed.count).to eq(3)
+ end
+ end
+end
diff --git a/spec/models/loose_foreign_keys/modification_tracker_spec.rb b/spec/models/loose_foreign_keys/modification_tracker_spec.rb
new file mode 100644
index 00000000000..069ccf85141
--- /dev/null
+++ b/spec/models/loose_foreign_keys/modification_tracker_spec.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe LooseForeignKeys::ModificationTracker do
+ subject(:tracker) { described_class.new }
+
+ describe '#over_limit?' do
+ it 'is true when deletion MAX_DELETES is exceeded' do
+ stub_const('LooseForeignKeys::ModificationTracker::MAX_DELETES', 5)
+
+ tracker.add_deletions('issues', 10)
+ expect(tracker).to be_over_limit
+ end
+
+ it 'is false when MAX_DELETES is not exceeded' do
+ tracker.add_deletions('issues', 3)
+
+ expect(tracker).not_to be_over_limit
+ end
+
+ it 'is true when deletion MAX_UPDATES is exceeded' do
+ stub_const('LooseForeignKeys::ModificationTracker::MAX_UPDATES', 5)
+
+ tracker.add_updates('issues', 3)
+ tracker.add_updates('issues', 4)
+
+ expect(tracker).to be_over_limit
+ end
+
+ it 'is false when MAX_UPDATES is not exceeded' do
+ tracker.add_updates('projects', 3)
+
+ expect(tracker).not_to be_over_limit
+ end
+
+ it 'is true when max runtime is exceeded' do
+ monotonic_time_before = 1 # this will be the start time
+ monotonic_time_after = described_class::MAX_RUNTIME.to_i + 1 # this will be returned when over_limit? is called
+
+ allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(monotonic_time_before, monotonic_time_after)
+
+ tracker
+
+ expect(tracker).to be_over_limit
+ end
+
+ it 'is false when max runtime is not exceeded' do
+ expect(tracker).not_to be_over_limit
+ end
+ end
+
+ describe '#add_deletions' do
+ it 'increments a Prometheus counter' do
+ counter = Gitlab::Metrics.registry.get(:loose_foreign_key_deletions)
+
+ subject.add_deletions(:users, 4)
+
+ expect(counter.get(table: :users)).to eq(4)
+ end
+ end
+
+ describe '#add_updates' do
+ it 'increments a Prometheus counter' do
+ counter = Gitlab::Metrics.registry.get(:loose_foreign_key_updates)
+
+ subject.add_updates(:users, 4)
+
+ expect(counter.get(table: :users)).to eq(4)
+ end
+ end
+
+ describe '#stats' do
+ it 'exposes stats' do
+ freeze_time do
+ tracker
+ tracker.add_deletions('issues', 5)
+ tracker.add_deletions('issues', 2)
+ tracker.add_deletions('projects', 2)
+
+ tracker.add_updates('projects', 3)
+
+ expect(tracker.stats).to eq({
+ over_limit: false,
+ delete_count_by_table: { 'issues' => 7, 'projects' => 2 },
+ update_count_by_table: { 'projects' => 3 },
+ delete_count: 9,
+ update_count: 3
+ })
+ end
+ end
+ end
+end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index afe78adc547..abff1815f1a 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe Member do
describe 'Associations' do
it { is_expected.to belong_to(:user) }
+ it { is_expected.to have_one(:member_task) }
end
describe 'Validation' do
@@ -678,6 +679,19 @@ RSpec.describe Member do
expect(member.invite_token).not_to be_nil
expect_any_instance_of(Member).not_to receive(:after_accept_invite)
end
+
+ it 'schedules a TasksToBeDone::CreateWorker task' do
+ stub_experiments(invite_members_for_task: true)
+
+ member_task = create(:member_task, member: member, project: member.project)
+
+ expect(TasksToBeDone::CreateWorker)
+ .to receive(:perform_async)
+ .with(member_task.id, member.created_by_id, [user.id])
+ .once
+
+ member.accept_invite!(user)
+ end
end
describe '#decline_invite!' do
diff --git a/spec/models/members/member_task_spec.rb b/spec/models/members/member_task_spec.rb
new file mode 100644
index 00000000000..b06aa05c255
--- /dev/null
+++ b/spec/models/members/member_task_spec.rb
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe MemberTask do
+ describe 'Associations' do
+ it { is_expected.to belong_to(:member) }
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe 'Validations' do
+ it { is_expected.to validate_presence_of(:member) }
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_inclusion_of(:tasks).in_array(MemberTask::TASKS.values) }
+
+ describe 'unique tasks validation' do
+ subject do
+ build(:member_task, tasks: [0, 0])
+ end
+
+ it 'expects the task values to be unique' do
+ expect(subject).to be_invalid
+ expect(subject.errors[:tasks]).to include('are not unique')
+ end
+ end
+
+ describe 'project validations' do
+ let_it_be(:project) { create(:project) }
+
+ subject do
+ build(:member_task, member: member, project: project, tasks_to_be_done: [:ci, :code])
+ end
+
+ context 'when the member source is a group' do
+ let_it_be(:member) { create(:group_member) }
+
+ it "expects the project to be part of the member's group projects" do
+ expect(subject).to be_invalid
+ expect(subject.errors[:project]).to include('is not in the member group')
+ end
+
+ context "when the project is part of the member's group projects" do
+ let_it_be(:project) { create(:project, namespace: member.source) }
+
+ it { is_expected.to be_valid }
+ end
+ end
+
+ context 'when the member source is a project' do
+ let_it_be(:member) { create(:project_member) }
+
+ it "expects the project to be the member's project" do
+ expect(subject).to be_invalid
+ expect(subject.errors[:project]).to include('is not the member project')
+ end
+
+ context "when the project is the member's project" do
+ let_it_be(:project) { member.source }
+
+ it { is_expected.to be_valid }
+ end
+ end
+ end
+ end
+
+ describe '.for_members' do
+ it 'returns the member_tasks for multiple members' do
+ member1 = create(:group_member)
+ member_task1 = create(:member_task, member: member1)
+ create(:member_task)
+ expect(described_class.for_members([member1])).to match_array([member_task1])
+ end
+ end
+
+ describe '#tasks_to_be_done' do
+ subject { member_task.tasks_to_be_done }
+
+ let_it_be(:member_task) { build(:member_task) }
+
+ before do
+ member_task[:tasks] = [0, 1]
+ end
+
+ it 'returns an array of symbols for the corresponding integers' do
+ expect(subject).to match_array([:ci, :code])
+ end
+ end
+
+ describe '#tasks_to_be_done=' do
+ let_it_be(:member_task) { build(:member_task) }
+
+ context 'when passing valid values' do
+ subject { member_task[:tasks] }
+
+ before do
+ member_task.tasks_to_be_done = tasks
+ end
+
+ context 'when passing tasks as strings' do
+ let_it_be(:tasks) { %w(ci code) }
+
+ it 'sets an array of integers for the corresponding tasks' do
+ expect(subject).to match_array([0, 1])
+ end
+ end
+
+ context 'when passing a single task' do
+ let_it_be(:tasks) { :ci }
+
+ it 'sets an array of integers for the corresponding tasks' do
+ expect(subject).to match_array([1])
+ end
+ end
+
+ context 'when passing a task twice' do
+ let_it_be(:tasks) { %w(ci ci) }
+
+ it 'is set only once' do
+ expect(subject).to match_array([1])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index ca846cf9e8e..031caefbd43 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -256,59 +256,5 @@ RSpec.describe ProjectMember do
it_behaves_like 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations'
end
-
- context 'when the feature flag `specialized_service_for_project_member_auth_refresh` is disabled' do
- before do
- stub_feature_flags(specialized_service_for_project_member_auth_refresh: false)
- end
-
- shared_examples_for 'calls UserProjectAccessChangedService to recalculate authorizations' do
- it 'calls UserProjectAccessChangedService' do
- expect_next_instance_of(UserProjectAccessChangedService, user.id) do |service|
- expect(service).to receive(:execute)
- end
-
- action
- end
- end
-
- context 'on create' do
- let(:action) { project.add_user(user, Gitlab::Access::GUEST) }
-
- it 'changes access level' do
- expect { action }.to change { user.can?(:guest_access, project) }.from(false).to(true)
- end
-
- it_behaves_like 'calls UserProjectAccessChangedService to recalculate authorizations'
- end
-
- context 'on update' do
- let(:action) { project.members.find_by(user: user).update!(access_level: Gitlab::Access::DEVELOPER) }
-
- before do
- project.add_user(user, Gitlab::Access::GUEST)
- end
-
- it 'changes access level' do
- expect { action }.to change { user.can?(:developer_access, project) }.from(false).to(true)
- end
-
- it_behaves_like 'calls UserProjectAccessChangedService to recalculate authorizations'
- end
-
- context 'on destroy' do
- let(:action) { project.members.find_by(user: user).destroy! }
-
- before do
- project.add_user(user, Gitlab::Access::GUEST)
- end
-
- it 'changes access level', :sidekiq_inline do
- expect { action }.to change { user.can?(:guest_access, project) }.from(true).to(false)
- end
-
- it_behaves_like 'calls UserProjectAccessChangedService to recalculate authorizations'
- end
- end
end
end
diff --git a/spec/models/merge_request_assignee_spec.rb b/spec/models/merge_request_assignee_spec.rb
index d287392bf7f..5bb8e7184a3 100644
--- a/spec/models/merge_request_assignee_spec.rb
+++ b/spec/models/merge_request_assignee_spec.rb
@@ -37,4 +37,8 @@ RSpec.describe MergeRequestAssignee do
end
end
end
+
+ it_behaves_like 'having unique enum values'
+
+ it_behaves_like 'having reviewer state'
end
diff --git a/spec/models/merge_request_diff_commit_spec.rb b/spec/models/merge_request_diff_commit_spec.rb
index adddec7ced8..25e5e40feb7 100644
--- a/spec/models/merge_request_diff_commit_spec.rb
+++ b/spec/models/merge_request_diff_commit_spec.rb
@@ -46,11 +46,7 @@ RSpec.describe MergeRequestDiffCommit do
{
"message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
"authored_date": "2014-02-27T10:01:38.000+01:00".to_time,
- "author_name": "Dmitriy Zaporozhets",
- "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T10:01:38.000+01:00".to_time,
- "committer_name": "Dmitriy Zaporozhets",
- "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author_id": an_instance_of(Integer),
"committer_id": an_instance_of(Integer),
"merge_request_diff_id": merge_request_diff_id,
@@ -61,11 +57,7 @@ RSpec.describe MergeRequestDiffCommit do
{
"message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
"authored_date": "2014-02-27T09:57:31.000+01:00".to_time,
- "author_name": "Dmitriy Zaporozhets",
- "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:57:31.000+01:00".to_time,
- "committer_name": "Dmitriy Zaporozhets",
- "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author_id": an_instance_of(Integer),
"committer_id": an_instance_of(Integer),
"merge_request_diff_id": merge_request_diff_id,
@@ -79,7 +71,7 @@ RSpec.describe MergeRequestDiffCommit do
subject { described_class.create_bulk(merge_request_diff_id, commits) }
it 'inserts the commits into the database en masse' do
- expect(Gitlab::Database.main).to receive(:bulk_insert)
+ expect(ApplicationRecord).to receive(:legacy_bulk_insert)
.with(described_class.table_name, rows)
subject
@@ -111,11 +103,7 @@ RSpec.describe MergeRequestDiffCommit do
[{
"message": "Weird commit date\n",
"authored_date": timestamp,
- "author_name": "Alejandro Rodríguez",
- "author_email": "alejorro70@gmail.com",
"committed_date": timestamp,
- "committer_name": "Alejandro Rodríguez",
- "committer_email": "alejorro70@gmail.com",
"commit_author_id": an_instance_of(Integer),
"committer_id": an_instance_of(Integer),
"merge_request_diff_id": merge_request_diff_id,
@@ -126,7 +114,7 @@ RSpec.describe MergeRequestDiffCommit do
end
it 'uses a sanitized date' do
- expect(Gitlab::Database.main).to receive(:bulk_insert)
+ expect(ApplicationRecord).to receive(:legacy_bulk_insert)
.with(described_class.table_name, rows)
subject
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index 5fff880c44e..afe7251f59a 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -240,8 +240,8 @@ RSpec.describe MergeRequestDiff do
stub_external_diffs_setting(enabled: true)
expect(diff).not_to receive(:save!)
- expect(Gitlab::Database.main)
- .to receive(:bulk_insert)
+ expect(ApplicationRecord)
+ .to receive(:legacy_bulk_insert)
.with('merge_request_diff_files', anything)
.and_raise(ActiveRecord::Rollback)
@@ -1080,6 +1080,22 @@ RSpec.describe MergeRequestDiff do
end
end
+ describe '#commits' do
+ include ProjectForksHelper
+
+ let_it_be(:target) { create(:project, :test_repo) }
+ let_it_be(:forked) { fork_project(target, nil, repository: true) }
+ let_it_be(:mr) { create(:merge_request, source_project: forked, target_project: target) }
+
+ it 'returns a CommitCollection whose container points to the target project' do
+ expect(mr.merge_request_diff.commits.container).to eq(target)
+ end
+
+ it 'returns a non-empty CommitCollection' do
+ expect(mr.merge_request_diff.commits.commits.size).to be > 0
+ end
+ end
+
describe '.latest_diff_for_merge_requests' do
let_it_be(:merge_request_1) { create(:merge_request_without_merge_request_diff) }
let_it_be(:merge_request_1_diff_1) { create(:merge_request_diff, merge_request: merge_request_1, created_at: 3.days.ago) }
diff --git a/spec/models/merge_request_reviewer_spec.rb b/spec/models/merge_request_reviewer_spec.rb
index 76b44abca54..d69d60c94f0 100644
--- a/spec/models/merge_request_reviewer_spec.rb
+++ b/spec/models/merge_request_reviewer_spec.rb
@@ -7,6 +7,10 @@ RSpec.describe MergeRequestReviewer do
subject { merge_request.merge_request_reviewers.build(reviewer: create(:user)) }
+ it_behaves_like 'having unique enum values'
+
+ it_behaves_like 'having reviewer state'
+
describe 'associations' do
it { is_expected.to belong_to(:merge_request).class_name('MergeRequest') }
it { is_expected.to belong_to(:reviewer).class_name('User').inverse_of(:merge_request_reviewers) }
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index d871453e062..5618fb06157 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -1638,6 +1638,22 @@ RSpec.describe MergeRequest, factory_default: :keep do
expect(request.default_merge_commit_message)
.not_to match("By removing all code\n\n")
end
+
+ it 'uses template from target project' do
+ request = build(:merge_request, title: 'Fix everything')
+ subject.target_project.merge_commit_template = '%{title}'
+
+ expect(request.default_merge_commit_message)
+ .to eq('Fix everything')
+ end
+
+ it 'ignores template when include_description is true' do
+ request = build(:merge_request, title: 'Fix everything')
+ subject.target_project.merge_commit_template = '%{title}'
+
+ expect(request.default_merge_commit_message(include_description: true))
+ .to match("See merge request #{request.to_reference(full: true)}")
+ end
end
describe "#auto_merge_strategy" do
@@ -2904,6 +2920,8 @@ RSpec.describe MergeRequest, factory_default: :keep do
params = {}
merge_jid = 'hash-123'
+ allow(MergeWorker).to receive(:with_status).and_return(MergeWorker)
+
expect(merge_request).to receive(:expire_etag_cache)
expect(MergeWorker).to receive(:perform_async).with(merge_request.id, user_id, params) do
merge_jid
@@ -2922,6 +2940,10 @@ RSpec.describe MergeRequest, factory_default: :keep do
subject(:execute) { merge_request.rebase_async(user_id) }
+ before do
+ allow(RebaseWorker).to receive(:with_status).and_return(RebaseWorker)
+ end
+
it 'atomically enqueues a RebaseWorker job and updates rebase_jid' do
expect(RebaseWorker)
.to receive(:perform_async)
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index c201d89947e..8f5860c799c 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -28,6 +28,41 @@ RSpec.describe Namespace do
it { is_expected.to have_one :onboarding_progress }
it { is_expected.to have_one :admin_note }
it { is_expected.to have_many :pending_builds }
+
+ describe '#children' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:subgroup) { create(:group, parent: group) }
+ let_it_be(:project_with_namespace) { create(:project, namespace: group) }
+
+ it 'excludes project namespaces' do
+ expect(project_with_namespace.project_namespace.parent).to eq(group)
+ expect(group.children).to match_array([subgroup])
+ end
+ end
+ end
+
+ shared_examples 'validations called by different namespace types' do |method|
+ using RSpec::Parameterized::TableSyntax
+
+ where(:namespace_type, :call_validation) do
+ :namespace | true
+ :group | true
+ :user_namespace | true
+ :project_namespace | false
+ end
+
+ with_them do
+ it 'conditionally runs given validation' do
+ namespace = build(namespace_type)
+ if call_validation
+ expect(namespace).to receive(method)
+ else
+ expect(namespace).not_to receive(method)
+ end
+
+ namespace.valid?
+ end
+ end
end
describe 'validations' do
@@ -50,10 +85,10 @@ RSpec.describe Namespace do
ref(:project_sti_name) | ref(:user_sti_name) | 'project namespace cannot be the parent of another namespace'
ref(:project_sti_name) | ref(:group_sti_name) | 'project namespace cannot be the parent of another namespace'
ref(:project_sti_name) | ref(:project_sti_name) | 'project namespace cannot be the parent of another namespace'
- ref(:group_sti_name) | ref(:user_sti_name) | 'cannot not be used for user namespace'
+ ref(:group_sti_name) | ref(:user_sti_name) | 'cannot be used for user namespace'
ref(:group_sti_name) | ref(:group_sti_name) | nil
ref(:group_sti_name) | ref(:project_sti_name) | nil
- ref(:user_sti_name) | ref(:user_sti_name) | 'cannot not be used for user namespace'
+ ref(:user_sti_name) | ref(:user_sti_name) | 'cannot be used for user namespace'
ref(:user_sti_name) | ref(:group_sti_name) | 'user namespace cannot be the parent of another namespace'
ref(:user_sti_name) | ref(:project_sti_name) | nil
end
@@ -102,14 +137,20 @@ RSpec.describe Namespace do
end
end
- it 'does not allow too deep nesting' do
- ancestors = (1..21).to_a
- group = build(:group)
+ describe '#nesting_level_allowed' do
+ context 'for a group' do
+ it 'does not allow too deep nesting' do
+ ancestors = (1..21).to_a
+ group = build(:group)
+
+ allow(group).to receive(:ancestors).and_return(ancestors)
- allow(group).to receive(:ancestors).and_return(ancestors)
+ expect(group).not_to be_valid
+ expect(group.errors[:parent_id].first).to eq('has too deep level of nesting')
+ end
+ end
- expect(group).not_to be_valid
- expect(group.errors[:parent_id].first).to eq('has too deep level of nesting')
+ it_behaves_like 'validations called by different namespace types', :nesting_level_allowed
end
describe 'reserved path validation' do
@@ -188,7 +229,7 @@ RSpec.describe Namespace do
expect(namespace.path).to eq('j')
- namespace.update(name: 'something new')
+ namespace.update!(name: 'something new')
expect(namespace).to be_valid
expect(namespace.name).to eq('something new')
@@ -199,8 +240,10 @@ RSpec.describe Namespace do
let(:namespace) { build(:project_namespace) }
it 'allows to update path to single char' do
- namespace = create(:project_namespace)
- namespace.update(path: 'j')
+ project = create(:project)
+ namespace = project.project_namespace
+
+ namespace.update!(path: 'j')
expect(namespace).to be_valid
end
@@ -244,7 +287,7 @@ RSpec.describe Namespace do
end
end
- context 'creating a default Namespace' do
+ context 'creating a Namespace with nil type' do
let(:namespace_type) { nil }
it 'is the correct type of namespace' do
@@ -255,7 +298,7 @@ RSpec.describe Namespace do
end
context 'creating an unknown Namespace type' do
- let(:namespace_type) { 'One' }
+ let(:namespace_type) { 'nonsense' }
it 'creates a default Namespace' do
expect(Namespace.find(namespace.id)).to be_a(Namespace)
@@ -273,8 +316,8 @@ RSpec.describe Namespace do
describe '.by_parent' do
it 'includes correct namespaces' do
- expect(described_class.by_parent(namespace1.id)).to eq([namespace1sub])
- expect(described_class.by_parent(namespace2.id)).to eq([namespace2sub])
+ expect(described_class.by_parent(namespace1.id)).to match_array([namespace1sub])
+ expect(described_class.by_parent(namespace2.id)).to match_array([namespace2sub])
expect(described_class.by_parent(nil)).to match_array([namespace, namespace1, namespace2])
end
end
@@ -302,9 +345,13 @@ RSpec.describe Namespace do
describe '.without_project_namespaces' do
let_it_be(:user_namespace) { create(:user_namespace) }
- let_it_be(:project_namespace) { create(:project_namespace) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:project_namespace) { project.project_namespace }
it 'excludes project namespaces' do
+ expect(project_namespace).not_to be_nil
+ expect(project_namespace.parent).not_to be_nil
+ expect(described_class.all).to include(project_namespace)
expect(described_class.without_project_namespaces).to match_array([namespace, namespace1, namespace2, namespace1sub, namespace2sub, user_namespace, project_namespace.parent])
end
end
@@ -519,6 +566,25 @@ RSpec.describe Namespace do
it 'returns namespaces with a matching route path regardless of the casing' do
expect(described_class.search('PARENT-PATH/NEW-PATH', include_parents: true)).to eq([second_group])
end
+
+ context 'with project namespaces' do
+ let_it_be(:project) { create(:project, namespace: parent_group, path: 'some-new-path') }
+ let_it_be(:project_namespace) { project.project_namespace }
+
+ it 'does not return project namespace' do
+ search_result = described_class.search('path')
+
+ expect(search_result).not_to include(project_namespace)
+ expect(search_result).to match_array([first_group, parent_group, second_group])
+ end
+
+ it 'does not return project namespace when including parents' do
+ search_result = described_class.search('path', include_parents: true)
+
+ expect(search_result).not_to include(project_namespace)
+ expect(search_result).to match_array([first_group, parent_group, second_group])
+ end
+ end
end
describe '.with_statistics' do
@@ -528,26 +594,30 @@ RSpec.describe Namespace do
create(:project,
namespace: namespace,
statistics: build(:project_statistics,
- namespace: namespace,
- repository_size: 101,
- wiki_size: 505,
- lfs_objects_size: 202,
- build_artifacts_size: 303,
- packages_size: 404,
- snippets_size: 605))
+ namespace: namespace,
+ repository_size: 101,
+ wiki_size: 505,
+ lfs_objects_size: 202,
+ build_artifacts_size: 303,
+ pipeline_artifacts_size: 707,
+ packages_size: 404,
+ snippets_size: 605,
+ uploads_size: 808))
end
let(:project2) do
create(:project,
namespace: namespace,
statistics: build(:project_statistics,
- namespace: namespace,
- repository_size: 10,
- wiki_size: 50,
- lfs_objects_size: 20,
- build_artifacts_size: 30,
- packages_size: 40,
- snippets_size: 60))
+ namespace: namespace,
+ repository_size: 10,
+ wiki_size: 50,
+ lfs_objects_size: 20,
+ build_artifacts_size: 30,
+ pipeline_artifacts_size: 70,
+ packages_size: 40,
+ snippets_size: 60,
+ uploads_size: 80))
end
it "sums all project storage counters in the namespace" do
@@ -555,13 +625,15 @@ RSpec.describe Namespace do
project2
statistics = described_class.with_statistics.find(namespace.id)
- expect(statistics.storage_size).to eq 2330
+ expect(statistics.storage_size).to eq 3995
expect(statistics.repository_size).to eq 111
expect(statistics.wiki_size).to eq 555
expect(statistics.lfs_objects_size).to eq 222
expect(statistics.build_artifacts_size).to eq 333
+ expect(statistics.pipeline_artifacts_size).to eq 777
expect(statistics.packages_size).to eq 444
expect(statistics.snippets_size).to eq 665
+ expect(statistics.uploads_size).to eq 888
end
it "correctly handles namespaces without projects" do
@@ -572,8 +644,10 @@ RSpec.describe Namespace do
expect(statistics.wiki_size).to eq 0
expect(statistics.lfs_objects_size).to eq 0
expect(statistics.build_artifacts_size).to eq 0
+ expect(statistics.pipeline_artifacts_size).to eq 0
expect(statistics.packages_size).to eq 0
expect(statistics.snippets_size).to eq 0
+ expect(statistics.uploads_size).to eq 0
end
end
@@ -673,7 +747,7 @@ RSpec.describe Namespace do
end
it "moves dir if path changed" do
- namespace.update(path: namespace.full_path + '_new')
+ namespace.update!(path: namespace.full_path + '_new')
expect(gitlab_shell.repository_exists?(project.repository_storage, "#{namespace.path}/#{project.path}.git")).to be_truthy
end
@@ -684,7 +758,7 @@ RSpec.describe Namespace do
expect(namespace).to receive(:write_projects_repository_config).and_raise('foo')
expect do
- namespace.update(path: namespace.full_path + '_new')
+ namespace.update!(path: namespace.full_path + '_new')
end.to raise_error('foo')
end
end
@@ -701,7 +775,7 @@ RSpec.describe Namespace do
end
expect(Gitlab::ErrorTracking).to receive(:should_raise_for_dev?).and_return(false) # like prod
- namespace.update(path: namespace.full_path + '_new')
+ namespace.update!(path: namespace.full_path + '_new')
end
end
end
@@ -931,7 +1005,7 @@ RSpec.describe Namespace do
it "repository directory remains unchanged if path changed" do
before_disk_path = project.disk_path
- namespace.update(path: namespace.full_path + '_new')
+ namespace.update!(path: namespace.full_path + '_new')
expect(before_disk_path).to eq(project.disk_path)
expect(gitlab_shell.repository_exists?(project.repository_storage, "#{project.disk_path}.git")).to be_truthy
@@ -946,7 +1020,7 @@ RSpec.describe Namespace do
let!(:legacy_project_in_subgroup) { create(:project, :legacy_storage, :repository, namespace: subgroup, name: 'foo3') }
it 'updates project full path in .git/config' do
- parent.update(path: 'mygroup_new')
+ parent.update!(path: 'mygroup_new')
expect(project_rugged(project_in_parent_group).config['gitlab.fullpath']).to eq "mygroup_new/#{project_in_parent_group.path}"
expect(project_rugged(hashed_project_in_subgroup).config['gitlab.fullpath']).to eq "mygroup_new/mysubgroup/#{hashed_project_in_subgroup.path}"
@@ -958,7 +1032,7 @@ RSpec.describe Namespace do
repository_hashed_project_in_subgroup = hashed_project_in_subgroup.project_repository
repository_legacy_project_in_subgroup = legacy_project_in_subgroup.project_repository
- parent.update(path: 'mygroup_moved')
+ parent.update!(path: 'mygroup_moved')
expect(repository_project_in_parent_group.reload.disk_path).to eq "mygroup_moved/#{project_in_parent_group.path}"
expect(repository_hashed_project_in_subgroup.reload.disk_path).to eq hashed_project_in_subgroup.disk_path
@@ -992,7 +1066,7 @@ RSpec.describe Namespace do
it 'renames its dirs when deleted' do
allow(GitlabShellWorker).to receive(:perform_in)
- namespace.destroy
+ namespace.destroy!
expect(File.exist?(deleted_path_in_dir)).to be(true)
end
@@ -1000,7 +1074,7 @@ RSpec.describe Namespace do
it 'schedules the namespace for deletion' do
expect(GitlabShellWorker).to receive(:perform_in).with(5.minutes, :rm_namespace, repository_storage, deleted_path)
- namespace.destroy
+ namespace.destroy!
end
context 'in sub-groups' do
@@ -1014,7 +1088,7 @@ RSpec.describe Namespace do
it 'renames its dirs when deleted' do
allow(GitlabShellWorker).to receive(:perform_in)
- child.destroy
+ child.destroy!
expect(File.exist?(deleted_path_in_dir)).to be(true)
end
@@ -1022,7 +1096,7 @@ RSpec.describe Namespace do
it 'schedules the namespace for deletion' do
expect(GitlabShellWorker).to receive(:perform_in).with(5.minutes, :rm_namespace, repository_storage, deleted_path)
- child.destroy
+ child.destroy!
end
end
end
@@ -1035,7 +1109,7 @@ RSpec.describe Namespace do
expect(File.exist?(path_in_dir)).to be(false)
- namespace.destroy
+ namespace.destroy!
expect(File.exist?(deleted_path_in_dir)).to be(false)
end
@@ -1293,6 +1367,7 @@ RSpec.describe Namespace do
context 'refreshing project access on updating share_with_group_lock' do
let(:group) { create(:group, share_with_group_lock: false) }
let(:project) { create(:project, :private, group: group) }
+ let(:another_project) { create(:project, :private, group: group) }
let_it_be(:shared_with_group_one) { create(:group) }
let_it_be(:shared_with_group_two) { create(:group) }
@@ -1305,6 +1380,7 @@ RSpec.describe Namespace do
shared_with_group_one.add_developer(group_one_user)
shared_with_group_two.add_developer(group_two_user)
create(:project_group_link, group: shared_with_group_one, project: project)
+ create(:project_group_link, group: shared_with_group_one, project: another_project)
create(:project_group_link, group: shared_with_group_two, project: project)
end
@@ -1312,6 +1388,9 @@ RSpec.describe Namespace do
expect(AuthorizedProjectUpdate::ProjectRecalculateWorker)
.to receive(:perform_async).with(project.id).once
+ expect(AuthorizedProjectUpdate::ProjectRecalculateWorker)
+ .to receive(:perform_async).with(another_project.id).once
+
execute_update
end
@@ -1344,11 +1423,23 @@ RSpec.describe Namespace do
stub_feature_flags(specialized_worker_for_group_lock_update_auth_recalculation: false)
end
- it 'refreshes the permissions of the members of the old and new namespace' do
+ it 'updates authorizations leading to users from shared groups losing access', :sidekiq_inline do
expect { execute_update }
.to change { group_one_user.authorized_projects.include?(project) }.from(true).to(false)
.and change { group_two_user.authorized_projects.include?(project) }.from(true).to(false)
end
+
+ it 'updates the authorizations in a non-blocking manner' do
+ expect(AuthorizedProjectsWorker).to(
+ receive(:bulk_perform_async)
+ .with([[group_one_user.id]])).once
+
+ expect(AuthorizedProjectsWorker).to(
+ receive(:bulk_perform_async)
+ .with([[group_two_user.id]])).once
+
+ execute_update
+ end
end
end
@@ -1544,7 +1635,7 @@ RSpec.describe Namespace do
it 'returns the path before last save' do
group = create(:group)
- group.update(parent: nil)
+ group.update!(parent: nil)
expect(group.full_path_before_last_save).to eq(group.path_before_last_save)
end
@@ -1555,7 +1646,7 @@ RSpec.describe Namespace do
group = create(:group, parent: nil)
parent = create(:group)
- group.update(parent: parent)
+ group.update!(parent: parent)
expect(group.full_path_before_last_save).to eq("#{group.path_before_last_save}")
end
@@ -1566,7 +1657,7 @@ RSpec.describe Namespace do
parent = create(:group)
group = create(:group, parent: parent)
- group.update(parent: nil)
+ group.update!(parent: nil)
expect(group.full_path_before_last_save).to eq("#{parent.full_path}/#{group.path}")
end
@@ -1578,7 +1669,7 @@ RSpec.describe Namespace do
group = create(:group, parent: parent)
new_parent = create(:group)
- group.update(parent: new_parent)
+ group.update!(parent: new_parent)
expect(group.full_path_before_last_save).to eq("#{parent.full_path}/#{group.path}")
end
@@ -1845,87 +1936,95 @@ RSpec.describe Namespace do
end
context 'with a parent' do
- context 'when parent has shared runners disabled' do
- let(:parent) { create(:group, :shared_runners_disabled) }
- let(:group) { build(:group, shared_runners_enabled: true, parent_id: parent.id) }
-
- it 'is invalid' do
- expect(group).to be_invalid
- expect(group.errors[:shared_runners_enabled]).to include('cannot be enabled because parent group has shared Runners disabled')
+ context 'when namespace is a group' do
+ context 'when parent has shared runners disabled' do
+ let(:parent) { create(:group, :shared_runners_disabled) }
+ let(:group) { build(:group, shared_runners_enabled: true, parent_id: parent.id) }
+
+ it 'is invalid' do
+ expect(group).to be_invalid
+ expect(group.errors[:shared_runners_enabled]).to include('cannot be enabled because parent group has shared Runners disabled')
+ end
end
- end
- context 'when parent has shared runners disabled but allows override' do
- let(:parent) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners) }
- let(:group) { build(:group, shared_runners_enabled: true, parent_id: parent.id) }
+ context 'when parent has shared runners disabled but allows override' do
+ let(:parent) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners) }
+ let(:group) { build(:group, shared_runners_enabled: true, parent_id: parent.id) }
- it 'is valid' do
- expect(group).to be_valid
+ it 'is valid' do
+ expect(group).to be_valid
+ end
end
- end
- context 'when parent has shared runners enabled' do
- let(:parent) { create(:group, shared_runners_enabled: true) }
- let(:group) { build(:group, shared_runners_enabled: true, parent_id: parent.id) }
+ context 'when parent has shared runners enabled' do
+ let(:parent) { create(:group, shared_runners_enabled: true) }
+ let(:group) { build(:group, shared_runners_enabled: true, parent_id: parent.id) }
- it 'is valid' do
- expect(group).to be_valid
+ it 'is valid' do
+ expect(group).to be_valid
+ end
end
end
end
+
+ it_behaves_like 'validations called by different namespace types', :changing_shared_runners_enabled_is_allowed
end
describe 'validation #changing_allow_descendants_override_disabled_shared_runners_is_allowed' do
- context 'without a parent' do
- context 'with shared runners disabled' do
- let(:namespace) { build(:namespace, :allow_descendants_override_disabled_shared_runners, :shared_runners_disabled) }
+ context 'when namespace is a group' do
+ context 'without a parent' do
+ context 'with shared runners disabled' do
+ let(:namespace) { build(:group, :allow_descendants_override_disabled_shared_runners, :shared_runners_disabled) }
- it 'is valid' do
- expect(namespace).to be_valid
+ it 'is valid' do
+ expect(namespace).to be_valid
+ end
end
- end
- context 'with shared runners enabled' do
- let(:namespace) { create(:namespace) }
+ context 'with shared runners enabled' do
+ let(:namespace) { create(:namespace) }
- it 'is invalid' do
- namespace.allow_descendants_override_disabled_shared_runners = true
+ it 'is invalid' do
+ namespace.allow_descendants_override_disabled_shared_runners = true
- expect(namespace).to be_invalid
- expect(namespace.errors[:allow_descendants_override_disabled_shared_runners]).to include('cannot be changed if shared runners are enabled')
+ expect(namespace).to be_invalid
+ expect(namespace.errors[:allow_descendants_override_disabled_shared_runners]).to include('cannot be changed if shared runners are enabled')
+ end
end
end
- end
- context 'with a parent' do
- context 'when parent does not allow shared runners' do
- let(:parent) { create(:group, :shared_runners_disabled) }
- let(:group) { build(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners, parent_id: parent.id) }
+ context 'with a parent' do
+ context 'when parent does not allow shared runners' do
+ let(:parent) { create(:group, :shared_runners_disabled) }
+ let(:group) { build(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners, parent_id: parent.id) }
- it 'is invalid' do
- expect(group).to be_invalid
- expect(group.errors[:allow_descendants_override_disabled_shared_runners]).to include('cannot be enabled because parent group does not allow it')
+ it 'is invalid' do
+ expect(group).to be_invalid
+ expect(group.errors[:allow_descendants_override_disabled_shared_runners]).to include('cannot be enabled because parent group does not allow it')
+ end
end
- end
- context 'when parent allows shared runners and setting to true' do
- let(:parent) { create(:group, shared_runners_enabled: true) }
- let(:group) { build(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners, parent_id: parent.id) }
+ context 'when parent allows shared runners and setting to true' do
+ let(:parent) { create(:group, shared_runners_enabled: true) }
+ let(:group) { build(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners, parent_id: parent.id) }
- it 'is valid' do
- expect(group).to be_valid
+ it 'is valid' do
+ expect(group).to be_valid
+ end
end
- end
- context 'when parent allows shared runners and setting to false' do
- let(:parent) { create(:group, shared_runners_enabled: true) }
- let(:group) { build(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false, parent_id: parent.id) }
+ context 'when parent allows shared runners and setting to false' do
+ let(:parent) { create(:group, shared_runners_enabled: true) }
+ let(:group) { build(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false, parent_id: parent.id) }
- it 'is valid' do
- expect(group).to be_valid
+ it 'is valid' do
+ expect(group).to be_valid
+ end
end
end
end
+
+ it_behaves_like 'validations called by different namespace types', :changing_allow_descendants_override_disabled_shared_runners_is_allowed
end
describe '#root?' do
diff --git a/spec/models/namespaces/project_namespace_spec.rb b/spec/models/namespaces/project_namespace_spec.rb
index f38e8aa85d0..4416c49f1bf 100644
--- a/spec/models/namespaces/project_namespace_spec.rb
+++ b/spec/models/namespaces/project_namespace_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe Namespaces::ProjectNamespace, type: :model do
# using delete rather than destroy due to `delete` skipping AR hooks/callbacks
# so it's ensured to work at the DB level. Uses ON DELETE CASCADE on foreign key
let_it_be(:project) { create(:project) }
- let_it_be(:project_namespace) { create(:project_namespace, project: project) }
+ let_it_be(:project_namespace) { project.project_namespace }
it 'also deletes the associated project' do
project_namespace.delete
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 0dd77967f25..9d9cca0678a 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -155,7 +155,7 @@ RSpec.describe Note do
expect(note).to receive(:notify_after_destroy).and_call_original
expect(note.noteable).to receive(:after_note_destroyed).with(note)
- note.destroy
+ note.destroy!
end
it 'does not error if noteable is nil' do
@@ -163,7 +163,7 @@ RSpec.describe Note do
expect(note).to receive(:notify_after_destroy).and_call_original
expect(note).to receive(:noteable).at_least(:once).and_return(nil)
- expect { note.destroy }.not_to raise_error
+ expect { note.destroy! }.not_to raise_error
end
end
end
@@ -226,8 +226,8 @@ RSpec.describe Note do
describe 'read' do
before do
- @p1.project_members.create(user: @u2, access_level: ProjectMember::GUEST)
- @p2.project_members.create(user: @u3, access_level: ProjectMember::GUEST)
+ @p1.project_members.create!(user: @u2, access_level: ProjectMember::GUEST)
+ @p2.project_members.create!(user: @u3, access_level: ProjectMember::GUEST)
end
it { expect(Ability.allowed?(@u1, :read_note, @p1)).to be_falsey }
@@ -237,8 +237,8 @@ RSpec.describe Note do
describe 'write' do
before do
- @p1.project_members.create(user: @u2, access_level: ProjectMember::DEVELOPER)
- @p2.project_members.create(user: @u3, access_level: ProjectMember::DEVELOPER)
+ @p1.project_members.create!(user: @u2, access_level: ProjectMember::DEVELOPER)
+ @p2.project_members.create!(user: @u3, access_level: ProjectMember::DEVELOPER)
end
it { expect(Ability.allowed?(@u1, :create_note, @p1)).to be_falsey }
@@ -248,9 +248,9 @@ RSpec.describe Note do
describe 'admin' do
before do
- @p1.project_members.create(user: @u1, access_level: ProjectMember::REPORTER)
- @p1.project_members.create(user: @u2, access_level: ProjectMember::MAINTAINER)
- @p2.project_members.create(user: @u3, access_level: ProjectMember::MAINTAINER)
+ @p1.project_members.create!(user: @u1, access_level: ProjectMember::REPORTER)
+ @p1.project_members.create!(user: @u2, access_level: ProjectMember::MAINTAINER)
+ @p2.project_members.create!(user: @u3, access_level: ProjectMember::MAINTAINER)
end
it { expect(Ability.allowed?(@u1, :admin_note, @p1)).to be_falsey }
@@ -1468,7 +1468,7 @@ RSpec.describe Note do
shared_examples 'assignee check' do
context 'when the provided user is one of the assignees' do
before do
- note.noteable.update(assignees: [user, create(:user)])
+ note.noteable.update!(assignees: [user, create(:user)])
end
it 'returns true' do
@@ -1480,7 +1480,7 @@ RSpec.describe Note do
shared_examples 'author check' do
context 'when the provided user is the author' do
before do
- note.noteable.update(author: user)
+ note.noteable.update!(author: user)
end
it 'returns true' do
diff --git a/spec/models/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb
index 3f1684327e7..cc601fb30c2 100644
--- a/spec/models/notification_setting_spec.rb
+++ b/spec/models/notification_setting_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe NotificationSetting do
notification_setting.merge_merge_request = "t"
notification_setting.close_merge_request = "nil"
notification_setting.reopen_merge_request = "false"
- notification_setting.save
+ notification_setting.save!
end
it "parses boolean before saving" do
@@ -52,12 +52,12 @@ RSpec.describe NotificationSetting do
context 'notification_email' do
let_it_be(:user) { create(:user) }
- subject { described_class.new(source_id: 1, source_type: 'Project', user_id: user.id) }
+ subject { build(:notification_setting, user_id: user.id) }
it 'allows to change email to verified one' do
email = create(:email, :confirmed, user: user)
- subject.update(notification_email: email.email)
+ subject.notification_email = email.email
expect(subject).to be_valid
end
@@ -65,13 +65,13 @@ RSpec.describe NotificationSetting do
it 'does not allow to change email to not verified one' do
email = create(:email, user: user)
- subject.update(notification_email: email.email)
+ subject.notification_email = email.email
expect(subject).to be_invalid
end
it 'allows to change email to empty one' do
- subject.update(notification_email: '')
+ subject.notification_email = ''
expect(subject).to be_valid
end
@@ -85,7 +85,7 @@ RSpec.describe NotificationSetting do
1.upto(4) do |i|
setting = create(:notification_setting, user: user)
- setting.project.update(pending_delete: true) if i.even?
+ setting.project.update!(pending_delete: true) if i.even?
end
end
diff --git a/spec/models/operations/feature_flags/strategy_spec.rb b/spec/models/operations/feature_flags/strategy_spec.rb
index 9289e3beab5..de1b9d2c855 100644
--- a/spec/models/operations/feature_flags/strategy_spec.rb
+++ b/spec/models/operations/feature_flags/strategy_spec.rb
@@ -20,8 +20,12 @@ RSpec.describe Operations::FeatureFlags::Strategy do
end
with_them do
it 'skips parameters validation' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: invalid_name, parameters: { bad: 'params' })
+ strategy = build(:operations_strategy,
+ feature_flag: feature_flag,
+ name: invalid_name,
+ parameters: { bad: 'params' })
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:name]).to eq(['strategy name is invalid'])
expect(strategy.errors[:parameters]).to be_empty
@@ -36,19 +40,24 @@ RSpec.describe Operations::FeatureFlags::Strategy do
end
with_them do
it 'must have valid parameters for the strategy' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'gradualRolloutUserId', parameters: invalid_parameters)
+ strategy = build(:operations_strategy,
+ :gradual_rollout,
+ feature_flag: feature_flag,
+ parameters: invalid_parameters)
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:parameters]).to eq(['parameters are invalid'])
end
end
it 'allows the parameters in any order' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'gradualRolloutUserId',
- parameters: { percentage: '10', groupId: 'mygroup' })
+ strategy = build(:operations_strategy,
+ :gradual_rollout,
+ feature_flag: feature_flag,
+ parameters: { percentage: '10', groupId: 'mygroup' })
- expect(strategy.errors[:parameters]).to be_empty
+ expect(strategy).to be_valid
end
describe 'percentage' do
@@ -59,9 +68,12 @@ RSpec.describe Operations::FeatureFlags::Strategy do
end
with_them do
it 'must be a string value between 0 and 100 inclusive and without a percentage sign' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'gradualRolloutUserId',
- parameters: { groupId: 'mygroup', percentage: invalid_value })
+ strategy = build(:operations_strategy,
+ :gradual_rollout,
+ feature_flag: feature_flag,
+ parameters: { groupId: 'mygroup', percentage: invalid_value })
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:parameters]).to eq(['percentage must be a string between 0 and 100 inclusive'])
end
@@ -72,11 +84,12 @@ RSpec.describe Operations::FeatureFlags::Strategy do
end
with_them do
it 'must be a string value between 0 and 100 inclusive and without a percentage sign' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'gradualRolloutUserId',
- parameters: { groupId: 'mygroup', percentage: valid_value })
+ strategy = build(:operations_strategy,
+ :gradual_rollout,
+ feature_flag: feature_flag,
+ parameters: { groupId: 'mygroup', percentage: valid_value })
- expect(strategy.errors[:parameters]).to eq([])
+ expect(strategy).to be_valid
end
end
end
@@ -88,9 +101,12 @@ RSpec.describe Operations::FeatureFlags::Strategy do
end
with_them do
it 'must be a string value of up to 32 lowercase characters' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'gradualRolloutUserId',
- parameters: { groupId: invalid_value, percentage: '40' })
+ strategy = build(:operations_strategy,
+ :gradual_rollout,
+ feature_flag: feature_flag,
+ parameters: { groupId: invalid_value, percentage: '40' })
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:parameters]).to eq(['groupId parameter is invalid'])
end
@@ -101,11 +117,12 @@ RSpec.describe Operations::FeatureFlags::Strategy do
end
with_them do
it 'must be a string value of up to 32 lowercase characters' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'gradualRolloutUserId',
- parameters: { groupId: valid_value, percentage: '40' })
+ strategy = build(:operations_strategy,
+ :gradual_rollout,
+ feature_flag: feature_flag,
+ parameters: { groupId: valid_value, percentage: '40' })
- expect(strategy.errors[:parameters]).to eq([])
+ expect(strategy).to be_valid
end
end
end
@@ -123,9 +140,12 @@ RSpec.describe Operations::FeatureFlags::Strategy do
])
with_them do
it 'must have valid parameters for the strategy' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'flexibleRollout',
- parameters: invalid_parameters)
+ strategy = build(:operations_strategy,
+ :flexible_rollout,
+ feature_flag: feature_flag,
+ parameters: invalid_parameters)
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:parameters]).to eq(['parameters are invalid'])
end
@@ -137,11 +157,12 @@ RSpec.describe Operations::FeatureFlags::Strategy do
[:groupId, 'mygroup']
].permutation(3).each do |parameters|
it "allows the parameters in the order #{parameters.map { |p| p.first }.join(', ')}" do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'flexibleRollout',
- parameters: Hash[parameters])
+ strategy = build(:operations_strategy,
+ :flexible_rollout,
+ feature_flag: feature_flag,
+ parameters: Hash[parameters])
- expect(strategy.errors[:parameters]).to be_empty
+ expect(strategy).to be_valid
end
end
@@ -152,9 +173,12 @@ RSpec.describe Operations::FeatureFlags::Strategy do
with_them do
it 'must be a string value between 0 and 100 inclusive and without a percentage sign' do
parameters = { stickiness: 'default', groupId: 'mygroup', rollout: invalid_value }
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'flexibleRollout',
- parameters: parameters)
+ strategy = build(:operations_strategy,
+ :flexible_rollout,
+ feature_flag: feature_flag,
+ parameters: parameters)
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:parameters]).to eq([
'rollout must be a string between 0 and 100 inclusive'
@@ -166,11 +190,12 @@ RSpec.describe Operations::FeatureFlags::Strategy do
with_them do
it 'must be a string value between 0 and 100 inclusive and without a percentage sign' do
parameters = { stickiness: 'default', groupId: 'mygroup', rollout: valid_value }
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'flexibleRollout',
- parameters: parameters)
+ strategy = build(:operations_strategy,
+ :flexible_rollout,
+ feature_flag: feature_flag,
+ parameters: parameters)
- expect(strategy.errors[:parameters]).to eq([])
+ expect(strategy).to be_valid
end
end
end
@@ -181,9 +206,12 @@ RSpec.describe Operations::FeatureFlags::Strategy do
with_them do
it 'must be a string value of up to 32 lowercase characters' do
parameters = { stickiness: 'default', groupId: invalid_value, rollout: '40' }
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'flexibleRollout',
- parameters: parameters)
+ strategy = build(:operations_strategy,
+ :flexible_rollout,
+ feature_flag: feature_flag,
+ parameters: parameters)
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:parameters]).to eq(['groupId parameter is invalid'])
end
@@ -193,11 +221,12 @@ RSpec.describe Operations::FeatureFlags::Strategy do
with_them do
it 'must be a string value of up to 32 lowercase characters' do
parameters = { stickiness: 'default', groupId: valid_value, rollout: '40' }
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'flexibleRollout',
- parameters: parameters)
+ strategy = build(:operations_strategy,
+ :flexible_rollout,
+ feature_flag: feature_flag,
+ parameters: parameters)
- expect(strategy.errors[:parameters]).to eq([])
+ expect(strategy).to be_valid
end
end
end
@@ -207,9 +236,12 @@ RSpec.describe Operations::FeatureFlags::Strategy do
with_them do
it 'must be a string representing a supported stickiness setting' do
parameters = { stickiness: invalid_value, groupId: 'mygroup', rollout: '40' }
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'flexibleRollout',
- parameters: parameters)
+ strategy = build(:operations_strategy,
+ :flexible_rollout,
+ feature_flag: feature_flag,
+ parameters: parameters)
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:parameters]).to eq([
'stickiness parameter must be default, userId, sessionId, or random'
@@ -221,11 +253,12 @@ RSpec.describe Operations::FeatureFlags::Strategy do
with_them do
it 'must be a string representing a supported stickiness setting' do
parameters = { stickiness: valid_value, groupId: 'mygroup', rollout: '40' }
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'flexibleRollout',
- parameters: parameters)
+ strategy = build(:operations_strategy,
+ :flexible_rollout,
+ feature_flag: feature_flag,
+ parameters: parameters)
- expect(strategy.errors[:parameters]).to eq([])
+ expect(strategy).to be_valid
end
end
end
@@ -237,8 +270,11 @@ RSpec.describe Operations::FeatureFlags::Strategy do
end
with_them do
it 'must have valid parameters for the strategy' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'userWithId', parameters: invalid_parameters)
+ strategy = build(:operations_strategy,
+ feature_flag: feature_flag,
+ name: 'userWithId', parameters: invalid_parameters)
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:parameters]).to eq(['parameters are invalid'])
end
@@ -253,10 +289,12 @@ RSpec.describe Operations::FeatureFlags::Strategy do
end
with_them do
it 'is valid with a string of comma separated values' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'userWithId', parameters: { userIds: valid_value })
+ strategy = build(:operations_strategy,
+ feature_flag: feature_flag,
+ name: 'userWithId',
+ parameters: { userIds: valid_value })
- expect(strategy.errors[:parameters]).to be_empty
+ expect(strategy).to be_valid
end
end
@@ -267,8 +305,12 @@ RSpec.describe Operations::FeatureFlags::Strategy do
end
with_them do
it 'is invalid' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'userWithId', parameters: { userIds: invalid_value })
+ strategy = build(:operations_strategy,
+ feature_flag: feature_flag,
+ name: 'userWithId',
+ parameters: { userIds: invalid_value })
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:parameters]).to include(
'userIds must be a string of unique comma separated values each 256 characters or less'
@@ -284,43 +326,48 @@ RSpec.describe Operations::FeatureFlags::Strategy do
end
with_them do
it 'must be empty' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'default',
- parameters: invalid_value)
+ strategy = build(:operations_strategy, :default, feature_flag: feature_flag, parameters: invalid_value)
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:parameters]).to eq(['parameters are invalid'])
end
end
it 'must be empty' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'default',
- parameters: {})
+ strategy = build(:operations_strategy, :default, feature_flag: feature_flag)
- expect(strategy.errors[:parameters]).to be_empty
+ expect(strategy).to be_valid
end
end
context 'when the strategy name is gitlabUserList' do
+ let_it_be(:user_list) { create(:operations_feature_flag_user_list, project: project) }
+
where(:invalid_value) do
[{ groupId: "default", percentage: "7" }, "", "nothing", 7, nil, [], 2.5, { userIds: 'user1' }]
end
with_them do
- it 'must be empty' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'gitlabUserList',
- parameters: invalid_value)
+ it 'is invalid' do
+ strategy = build(:operations_strategy,
+ :gitlab_userlist,
+ user_list: user_list,
+ feature_flag: feature_flag,
+ parameters: invalid_value)
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:parameters]).to eq(['parameters are invalid'])
end
end
- it 'must be empty' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'gitlabUserList',
- parameters: {})
+ it 'is valid' do
+ strategy = build(:operations_strategy,
+ :gitlab_userlist,
+ user_list: user_list,
+ feature_flag: feature_flag)
- expect(strategy.errors[:parameters]).to be_empty
+ expect(strategy).to be_valid
end
end
end
@@ -329,18 +376,15 @@ RSpec.describe Operations::FeatureFlags::Strategy do
context 'when name is gitlabUserList' do
it 'is valid when associated with a user list' do
user_list = create(:operations_feature_flag_user_list, project: project)
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'gitlabUserList',
- user_list: user_list,
- parameters: {})
+ strategy = build(:operations_strategy, :gitlab_userlist, feature_flag: feature_flag, user_list: user_list)
- expect(strategy.errors[:user_list]).to be_empty
+ expect(strategy).to be_valid
end
it 'is invalid without a user list' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'gitlabUserList',
- parameters: {})
+ strategy = build(:operations_strategy, :gitlab_userlist, feature_flag: feature_flag, user_list: nil)
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:user_list]).to eq(["can't be blank"])
end
@@ -348,10 +392,9 @@ RSpec.describe Operations::FeatureFlags::Strategy do
it 'is invalid when associated with a user list from another project' do
other_project = create(:project)
user_list = create(:operations_feature_flag_user_list, project: other_project)
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'gitlabUserList',
- user_list: user_list,
- parameters: {})
+ strategy = build(:operations_strategy, :gitlab_userlist, feature_flag: feature_flag, user_list: user_list)
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:user_list]).to eq(['must belong to the same project'])
end
@@ -360,84 +403,68 @@ RSpec.describe Operations::FeatureFlags::Strategy do
context 'when name is default' do
it 'is invalid when associated with a user list' do
user_list = create(:operations_feature_flag_user_list, project: project)
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'default',
- user_list: user_list,
- parameters: {})
+ strategy = build(:operations_strategy, :default, feature_flag: feature_flag, user_list: user_list)
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:user_list]).to eq(['must be blank'])
end
it 'is valid without a user list' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'default',
- parameters: {})
+ strategy = build(:operations_strategy, :default, feature_flag: feature_flag)
- expect(strategy.errors[:user_list]).to be_empty
+ expect(strategy).to be_valid
end
end
context 'when name is userWithId' do
it 'is invalid when associated with a user list' do
user_list = create(:operations_feature_flag_user_list, project: project)
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'userWithId',
- user_list: user_list,
- parameters: { userIds: 'user1' })
+ strategy = build(:operations_strategy, :userwithid, feature_flag: feature_flag, user_list: user_list)
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:user_list]).to eq(['must be blank'])
end
it 'is valid without a user list' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'userWithId',
- parameters: { userIds: 'user1' })
+ strategy = build(:operations_strategy, :userwithid, feature_flag: feature_flag)
- expect(strategy.errors[:user_list]).to be_empty
+ expect(strategy).to be_valid
end
end
context 'when name is gradualRolloutUserId' do
it 'is invalid when associated with a user list' do
user_list = create(:operations_feature_flag_user_list, project: project)
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'gradualRolloutUserId',
- user_list: user_list,
- parameters: { groupId: 'default', percentage: '10' })
+ strategy = build(:operations_strategy, :gradual_rollout, feature_flag: feature_flag, user_list: user_list)
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:user_list]).to eq(['must be blank'])
end
it 'is valid without a user list' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'gradualRolloutUserId',
- parameters: { groupId: 'default', percentage: '10' })
+ strategy = build(:operations_strategy, :gradual_rollout, feature_flag: feature_flag)
- expect(strategy.errors[:user_list]).to be_empty
+ expect(strategy).to be_valid
end
end
context 'when name is flexibleRollout' do
it 'is invalid when associated with a user list' do
user_list = create(:operations_feature_flag_user_list, project: project)
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'flexibleRollout',
- user_list: user_list,
- parameters: { groupId: 'default',
- rollout: '10',
- stickiness: 'default' })
+ strategy = build(:operations_strategy, :flexible_rollout, feature_flag: feature_flag, user_list: user_list)
+
+ expect(strategy).to be_invalid
expect(strategy.errors[:user_list]).to eq(['must be blank'])
end
it 'is valid without a user list' do
- strategy = described_class.create(feature_flag: feature_flag,
- name: 'flexibleRollout',
- parameters: { groupId: 'default',
- rollout: '10',
- stickiness: 'default' })
+ strategy = build(:operations_strategy, :flexible_rollout, feature_flag: feature_flag)
- expect(strategy.errors[:user_list]).to be_empty
+ expect(strategy).to be_valid
end
end
end
diff --git a/spec/models/operations/feature_flags/user_list_spec.rb b/spec/models/operations/feature_flags/user_list_spec.rb
index 3a48d3389a3..b2dbebb2c0d 100644
--- a/spec/models/operations/feature_flags/user_list_spec.rb
+++ b/spec/models/operations/feature_flags/user_list_spec.rb
@@ -20,9 +20,9 @@ RSpec.describe Operations::FeatureFlags::UserList do
end
with_them do
it 'is valid with a string of comma separated values' do
- user_list = described_class.create(user_xids: valid_value)
+ user_list = build(:operations_feature_flag_user_list, user_xids: valid_value)
- expect(user_list.errors[:user_xids]).to be_empty
+ expect(user_list).to be_valid
end
end
@@ -31,9 +31,10 @@ RSpec.describe Operations::FeatureFlags::UserList do
end
with_them do
it 'automatically casts values of other types' do
- user_list = described_class.create(user_xids: typecast_value)
+ user_list = build(:operations_feature_flag_user_list, user_xids: typecast_value)
+
+ expect(user_list).to be_valid
- expect(user_list.errors[:user_xids]).to be_empty
expect(user_list.user_xids).to eq(typecast_value.to_s)
end
end
@@ -45,7 +46,9 @@ RSpec.describe Operations::FeatureFlags::UserList do
end
with_them do
it 'is invalid' do
- user_list = described_class.create(user_xids: invalid_value)
+ user_list = build(:operations_feature_flag_user_list, user_xids: invalid_value)
+
+ expect(user_list).to be_invalid
expect(user_list.errors[:user_xids]).to include(
'user_xids must be a string of unique comma separated values each 256 characters or less'
@@ -70,20 +73,20 @@ RSpec.describe Operations::FeatureFlags::UserList do
describe '#destroy' do
it 'deletes the model if it is not associated with any feature flag strategies' do
project = create(:project)
- user_list = described_class.create(project: project, name: 'My User List', user_xids: 'user1,user2')
+ user_list = described_class.create!(project: project, name: 'My User List', user_xids: 'user1,user2')
- user_list.destroy
+ user_list.destroy!
expect(described_class.count).to eq(0)
end
it 'does not delete the model if it is associated with a feature flag strategy' do
project = create(:project)
- user_list = described_class.create(project: project, name: 'My User List', user_xids: 'user1,user2')
+ user_list = described_class.create!(project: project, name: 'My User List', user_xids: 'user1,user2')
feature_flag = create(:operations_feature_flag, :new_version_flag, project: project)
strategy = create(:operations_strategy, feature_flag: feature_flag, name: 'gitlabUserList', user_list: user_list)
- user_list.destroy
+ user_list.destroy # rubocop:disable Rails/SaveBang
expect(described_class.count).to eq(1)
expect(::Operations::FeatureFlags::StrategyUserList.count).to eq(1)
diff --git a/spec/models/packages/npm/metadatum_spec.rb b/spec/models/packages/npm/metadatum_spec.rb
new file mode 100644
index 00000000000..ff8cce5310e
--- /dev/null
+++ b/spec/models/packages/npm/metadatum_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Npm::Metadatum, type: :model do
+ describe 'relationships' do
+ it { is_expected.to belong_to(:package).inverse_of(:npm_metadatum) }
+ end
+
+ describe 'validations' do
+ describe 'package', :aggregate_failures do
+ it { is_expected.to validate_presence_of(:package) }
+
+ it 'ensure npm package type' do
+ metadatum = build(:npm_metadatum)
+
+ metadatum.package = build(:nuget_package)
+
+ expect(metadatum).not_to be_valid
+ expect(metadatum.errors).to contain_exactly('Package type must be NPM')
+ end
+ end
+
+ describe 'package_json', :aggregate_failures do
+ let(:valid_json) { { 'name' => 'foo', 'version' => 'v1.0', 'dist' => { 'tarball' => 'x', 'shasum' => 'x' } } }
+
+ it { is_expected.to allow_value(valid_json).for(:package_json) }
+ it { is_expected.to allow_value(valid_json.merge('extra-field': { 'foo': 'bar' })).for(:package_json) }
+ it { is_expected.to allow_value(with_dist { |dist| dist.merge('extra-field': 'x') }).for(:package_json) }
+
+ %w[name version dist].each do |field|
+ it { is_expected.not_to allow_value(valid_json.except(field)).for(:package_json) }
+ end
+
+ %w[tarball shasum].each do |field|
+ it { is_expected.not_to allow_value(with_dist { |dist| dist.except(field) }).for(:package_json) }
+ end
+
+ it { is_expected.not_to allow_value({}).for(:package_json) }
+
+ it { is_expected.not_to allow_value(test: 'test' * 10000).for(:package_json) }
+
+ def with_dist
+ valid_json.tap do |h|
+ h['dist'] = yield(h['dist'])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/packages/package_file_spec.rb b/spec/models/packages/package_file_spec.rb
index 450656e3e9c..8617793f41d 100644
--- a/spec/models/packages/package_file_spec.rb
+++ b/spec/models/packages/package_file_spec.rb
@@ -14,7 +14,6 @@ RSpec.describe Packages::PackageFile, type: :model do
it { is_expected.to belong_to(:package) }
it { is_expected.to have_one(:conan_file_metadatum) }
it { is_expected.to have_many(:package_file_build_infos).inverse_of(:package_file) }
- it { is_expected.to have_many(:pipelines).through(:package_file_build_infos) }
it { is_expected.to have_one(:debian_file_metadatum).inverse_of(:package_file).class_name('Packages::Debian::FileMetadatum') }
it { is_expected.to have_one(:helm_file_metadatum).inverse_of(:package_file).class_name('Packages::Helm::FileMetadatum') }
end
@@ -206,6 +205,28 @@ RSpec.describe Packages::PackageFile, type: :model do
end
end
+ describe '#pipelines' do
+ let_it_be_with_refind(:package_file) { create(:package_file) }
+
+ subject { package_file.pipelines }
+
+ context 'package_file without pipeline' do
+ it { is_expected.to be_empty }
+ end
+
+ context 'package_file with pipeline' do
+ let_it_be(:pipeline) { create(:ci_pipeline) }
+ let_it_be(:pipeline2) { create(:ci_pipeline) }
+
+ before do
+ package_file.package_file_build_infos.create!(pipeline: pipeline)
+ package_file.package_file_build_infos.create!(pipeline: pipeline2)
+ end
+
+ it { is_expected.to contain_exactly(pipeline, pipeline2) }
+ end
+ end
+
describe '#update_file_store callback' do
let_it_be(:package_file) { build(:package_file, :nuget, size: nil) }
diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb
index 2573c01d686..6ee5219819c 100644
--- a/spec/models/packages/package_spec.rb
+++ b/spec/models/packages/package_spec.rb
@@ -14,13 +14,13 @@ RSpec.describe Packages::Package, type: :model do
it { is_expected.to have_many(:dependency_links).inverse_of(:package) }
it { is_expected.to have_many(:tags).inverse_of(:package) }
it { is_expected.to have_many(:build_infos).inverse_of(:package) }
- 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) }
+ it { is_expected.to have_one(:npm_metadatum).inverse_of(:package) }
end
describe '.with_debian_codename' do
@@ -999,6 +999,28 @@ RSpec.describe Packages::Package, type: :model do
end
end
+ describe '#pipelines' do
+ let_it_be_with_refind(:package) { create(:maven_package) }
+
+ subject { package.pipelines }
+
+ context 'package without pipeline' do
+ it { is_expected.to be_empty }
+ end
+
+ context 'package with pipeline' do
+ let_it_be(:pipeline) { create(:ci_pipeline) }
+ let_it_be(:pipeline2) { create(:ci_pipeline) }
+
+ before do
+ package.build_infos.create!(pipeline: pipeline)
+ package.build_infos.create!(pipeline: pipeline2)
+ end
+
+ it { is_expected.to contain_exactly(pipeline, pipeline2) }
+ end
+ end
+
describe '#tag_names' do
let_it_be(:package) { create(:nuget_package) }
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index 2b6ed9a9927..d476e18a72c 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -104,7 +104,7 @@ RSpec.describe PagesDomain do
let(:domain) { build(:pages_domain) }
it 'saves validity time' do
- domain.save
+ domain.save!
expect(domain.certificate_valid_not_before).to be_like_time(Time.zone.parse("2020-03-16 14:20:34 UTC"))
expect(domain.certificate_valid_not_after).to be_like_time(Time.zone.parse("2220-01-28 14:20:34 UTC"))
@@ -161,7 +161,7 @@ RSpec.describe PagesDomain do
context 'when certificate is already saved' do
it "doesn't add error to certificate" do
- domain.save(validate: false)
+ domain.save!(validate: false)
domain.valid?
diff --git a/spec/models/preloaders/group_policy_preloader_spec.rb b/spec/models/preloaders/group_policy_preloader_spec.rb
new file mode 100644
index 00000000000..f6e40d1f033
--- /dev/null
+++ b/spec/models/preloaders/group_policy_preloader_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Preloaders::GroupPolicyPreloader do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:root_parent) { create(:group, :private, name: 'root-1', path: 'root-1') }
+ let_it_be(:guest_group) { create(:group, name: 'public guest', path: 'public-guest') }
+ let_it_be(:private_maintainer_group) { create(:group, :private, name: 'b private maintainer', path: 'b-private-maintainer', parent: root_parent) }
+ let_it_be(:private_developer_group) { create(:group, :private, project_creation_level: nil, name: 'c public developer', path: 'c-public-developer') }
+ let_it_be(:public_maintainer_group) { create(:group, :private, name: 'a public maintainer', path: 'a-public-maintainer') }
+
+ let(:base_groups) { [guest_group, private_maintainer_group, private_developer_group, public_maintainer_group] }
+
+ before_all do
+ guest_group.add_guest(user)
+ private_maintainer_group.add_maintainer(user)
+ private_developer_group.add_developer(user)
+ public_maintainer_group.add_maintainer(user)
+ end
+
+ it 'avoids N+1 queries when authorizing a list of groups', :request_store do
+ preload_groups_for_policy(user)
+ control = ActiveRecord::QueryRecorder.new { authorize_all_groups(user) }
+
+ new_group1 = create(:group, :private).tap { |group| group.add_maintainer(user) }
+ new_group2 = create(:group, :private, parent: private_maintainer_group)
+
+ another_root = create(:group, :private, name: 'root-3', path: 'root-3')
+ new_group3 = create(:group, :private, parent: another_root).tap { |group| group.add_maintainer(user) }
+
+ pristine_groups = Group.where(id: base_groups + [new_group1, new_group2, new_group3])
+
+ preload_groups_for_policy(user, pristine_groups)
+ expect { authorize_all_groups(user, pristine_groups) }.not_to exceed_query_limit(control)
+ end
+
+ def authorize_all_groups(current_user, group_list = base_groups)
+ group_list.each { |group| current_user.can?(:read_group, group) }
+ end
+
+ def preload_groups_for_policy(current_user, group_list = base_groups)
+ described_class.new(group_list, current_user).execute
+ end
+end
diff --git a/spec/models/preloaders/group_root_ancestor_preloader_spec.rb b/spec/models/preloaders/group_root_ancestor_preloader_spec.rb
new file mode 100644
index 00000000000..0d622e84ef1
--- /dev/null
+++ b/spec/models/preloaders/group_root_ancestor_preloader_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Preloaders::GroupRootAncestorPreloader do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:root_parent1) { create(:group, :private, name: 'root-1', path: 'root-1') }
+ let_it_be(:root_parent2) { create(:group, :private, name: 'root-2', path: 'root-2') }
+ let_it_be(:guest_group) { create(:group, name: 'public guest', path: 'public-guest') }
+ let_it_be(:private_maintainer_group) { create(:group, :private, name: 'b private maintainer', path: 'b-private-maintainer', parent: root_parent1) }
+ let_it_be(:private_developer_group) { create(:group, :private, project_creation_level: nil, name: 'c public developer', path: 'c-public-developer') }
+ let_it_be(:public_maintainer_group) { create(:group, :private, name: 'a public maintainer', path: 'a-public-maintainer', parent: root_parent2) }
+
+ let(:root_query_regex) { /\ASELECT.+FROM "namespaces" WHERE "namespaces"."id" = \d+/ }
+ let(:additional_preloads) { [] }
+ let(:groups) { [guest_group, private_maintainer_group, private_developer_group, public_maintainer_group] }
+ let(:pristine_groups) { Group.where(id: groups) }
+
+ shared_examples 'executes N matching DB queries' do |expected_query_count, query_method = nil|
+ it 'executes the specified root_ancestor queries' do
+ expect do
+ pristine_groups.each do |group|
+ root_ancestor = group.root_ancestor
+
+ root_ancestor.public_send(query_method) if query_method.present?
+ end
+ end.to make_queries_matching(root_query_regex, expected_query_count)
+ end
+
+ it 'strong_memoizes the correct root_ancestor' do
+ pristine_groups.each do |group|
+ expected_parent_id = group.root_ancestor.id == group.id ? nil : group.root_ancestor.id
+
+ expect(group.parent_id).to eq(expected_parent_id)
+ end
+ end
+ end
+
+ context 'when the preloader is used' do
+ before do
+ preload_ancestors
+ end
+
+ context 'when no additional preloads are provided' do
+ it_behaves_like 'executes N matching DB queries', 0
+ end
+
+ context 'when additional preloads are provided' do
+ let(:additional_preloads) { [:route] }
+ let(:root_query_regex) { /\ASELECT.+FROM "routes" WHERE "routes"."source_id" = \d+/ }
+
+ it_behaves_like 'executes N matching DB queries', 0, :full_path
+ end
+ end
+
+ context 'when the preloader is not used' do
+ it_behaves_like 'executes N matching DB queries', 2
+ end
+
+ def preload_ancestors
+ described_class.new(pristine_groups, additional_preloads).execute
+ end
+end
diff --git a/spec/models/preloaders/user_max_access_level_in_groups_preloader_spec.rb b/spec/models/preloaders/user_max_access_level_in_groups_preloader_spec.rb
index 8144e1ad233..5fc7bfb1f62 100644
--- a/spec/models/preloaders/user_max_access_level_in_groups_preloader_spec.rb
+++ b/spec/models/preloaders/user_max_access_level_in_groups_preloader_spec.rb
@@ -13,32 +13,47 @@ RSpec.describe Preloaders::UserMaxAccessLevelInGroupsPreloader do
shared_examples 'executes N max member permission queries to the DB' do
it 'executes the specified max membership queries' do
- queries = ActiveRecord::QueryRecorder.new do
- groups.each { |group| user.can?(:read_group, group) }
- end
+ expect { groups.each { |group| user.can?(:read_group, group) } }.to make_queries_matching(max_query_regex, expected_query_count)
+ end
- max_queries = queries.log.grep(max_query_regex)
+ it 'caches the correct access_level for each group' do
+ groups.each do |group|
+ access_level_from_db = group.members_with_parents.where(user_id: user.id).group(:user_id).maximum(:access_level)[user.id] || Gitlab::Access::NO_ACCESS
+ cached_access_level = group.max_member_access_for_user(user)
- expect(max_queries.count).to eq(expected_query_count)
+ expect(cached_access_level).to eq(access_level_from_db)
+ end
end
end
context 'when the preloader is used', :request_store do
- before do
- described_class.new(groups, user).execute
- end
+ context 'when user has indirect access to groups' do
+ let_it_be(:child_maintainer) { create(:group, :private, parent: group1).tap {|g| g.add_maintainer(user)} }
+ let_it_be(:child_indirect_access) { create(:group, :private, parent: group1) }
- it_behaves_like 'executes N max member permission queries to the DB' do
- # Will query all groups where the user is not already a member
- let(:expected_query_count) { 1 }
- end
+ let(:groups) { [group1, group2, group3, child_maintainer, child_indirect_access] }
+
+ context 'when traversal_ids feature flag is disabled' do
+ it_behaves_like 'executes N max member permission queries to the DB' do
+ before do
+ stub_feature_flags(use_traversal_ids: false)
+ described_class.new(groups, user).execute
+ end
+
+ # One query for group with no access and another one per group where the user is not a direct member
+ let(:expected_query_count) { 2 }
+ end
+ end
- context 'when user has access but is not a direct member of the group' do
- let(:groups) { [group1, group2, group3, create(:group, :private, parent: group1)] }
+ context 'when traversal_ids feature flag is enabled' do
+ it_behaves_like 'executes N max member permission queries to the DB' do
+ before do
+ stub_feature_flags(use_traversal_ids: true)
+ described_class.new(groups, user).execute
+ end
- it_behaves_like 'executes N max member permission queries to the DB' do
- # One query for group with no access and another one where the user is not a direct member
- let(:expected_query_count) { 2 }
+ let(:expected_query_count) { 0 }
+ end
end
end
end
diff --git a/spec/models/project_authorization_spec.rb b/spec/models/project_authorization_spec.rb
index c517fc8be55..58c0ff48b46 100644
--- a/spec/models/project_authorization_spec.rb
+++ b/spec/models/project_authorization_spec.rb
@@ -3,9 +3,10 @@
require 'spec_helper'
RSpec.describe ProjectAuthorization do
- let(:user) { create(:user) }
- let(:project1) { create(:project) }
- let(:project2) { create(:project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project1) { create(:project) }
+ let_it_be(:project2) { create(:project) }
+ let_it_be(:project3) { create(:project) }
describe '.insert_authorizations' do
it 'inserts the authorizations' do
@@ -23,5 +24,19 @@ RSpec.describe ProjectAuthorization do
expect(user.project_authorizations.count).to eq(2)
end
+
+ it 'skips duplicates and inserts the remaining rows without error' do
+ create(:project_authorization, user: user, project: project1, access_level: Gitlab::Access::MAINTAINER)
+
+ rows = [
+ [user.id, project1.id, Gitlab::Access::MAINTAINER],
+ [user.id, project2.id, Gitlab::Access::MAINTAINER],
+ [user.id, project3.id, Gitlab::Access::MAINTAINER]
+ ]
+
+ described_class.insert_authorizations(rows)
+
+ expect(user.project_authorizations.pluck(:user_id, :project_id, :access_level)).to match_array(rows)
+ end
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 2e5c5af4eb0..3a8768ff463 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe Project, factory_default: :keep do
describe 'associations' do
it { is_expected.to belong_to(:group) }
it { is_expected.to belong_to(:namespace) }
- it { is_expected.to belong_to(:project_namespace).class_name('Namespaces::ProjectNamespace').with_foreign_key('project_namespace_id').inverse_of(:project) }
+ it { is_expected.to belong_to(:project_namespace).class_name('Namespaces::ProjectNamespace').with_foreign_key('project_namespace_id') }
it { is_expected.to belong_to(:creator).class_name('User') }
it { is_expected.to belong_to(:pool_repository) }
it { is_expected.to have_many(:users) }
@@ -191,7 +191,7 @@ RSpec.describe Project, factory_default: :keep do
# using delete rather than destroy due to `delete` skipping AR hooks/callbacks
# so it's ensured to work at the DB level. Uses AFTER DELETE trigger.
let_it_be(:project) { create(:project) }
- let_it_be(:project_namespace) { create(:project_namespace, project: project) }
+ let_it_be(:project_namespace) { project.project_namespace }
it 'also deletes the associated ProjectNamespace' do
project.delete
@@ -233,6 +233,58 @@ RSpec.describe Project, factory_default: :keep do
expect(project.project_setting).to be_an_instance_of(ProjectSetting)
expect(project.project_setting).to be_new_record
end
+
+ context 'with project namespaces' do
+ it 'automatically creates a project namespace' do
+ project = build(:project, path: 'hopefully-valid-path1')
+ project.save!
+
+ expect(project).to be_persisted
+ expect(project.project_namespace).to be_persisted
+ expect(project.project_namespace).to be_in_sync_with_project(project)
+ end
+
+ context 'with FF disabled' do
+ before do
+ stub_feature_flags(create_project_namespace_on_project_create: false)
+ end
+
+ it 'does not create a project namespace' do
+ project = build(:project, path: 'hopefully-valid-path2')
+ project.save!
+
+ expect(project).to be_persisted
+ expect(project.project_namespace).to be_nil
+ end
+ end
+ end
+ end
+
+ context 'updating a project' do
+ context 'with project namespaces' do
+ it 'keeps project namespace in sync with project' do
+ project = create(:project)
+ project.update!(path: 'hopefully-valid-path1')
+
+ expect(project).to be_persisted
+ expect(project.project_namespace).to be_persisted
+ expect(project.project_namespace).to be_in_sync_with_project(project)
+ end
+
+ context 'with FF disabled' do
+ before do
+ stub_feature_flags(create_project_namespace_on_project_create: false)
+ end
+
+ it 'does not create a project namespace when project is updated' do
+ project = create(:project)
+ project.update!(path: 'hopefully-valid-path1')
+
+ expect(project).to be_persisted
+ expect(project.project_namespace).to be_nil
+ end
+ end
+ end
end
context 'updating cd_cd_settings' do
@@ -294,6 +346,7 @@ RSpec.describe Project, factory_default: :keep do
it { is_expected.to validate_presence_of(:namespace) }
it { is_expected.to validate_presence_of(:repository_storage) }
it { is_expected.to validate_numericality_of(:max_artifacts_size).only_integer.is_greater_than(0) }
+ it { is_expected.to validate_length_of(:suggestion_commit_message).is_at_most(255) }
it 'validates build timeout constraints' do
is_expected.to validate_numericality_of(:build_timeout)
@@ -322,6 +375,18 @@ RSpec.describe Project, factory_default: :keep do
create(:project)
end
+ context 'validates project namespace creation' do
+ it 'does not create project namespace if project is not created' do
+ project = build(:project, path: 'tree')
+
+ project.valid?
+
+ expect(project).not_to be_valid
+ expect(project).to be_new_record
+ expect(project.project_namespace).to be_new_record
+ end
+ end
+
context 'repository storages inclusion' do
let(:project2) { build(:project, repository_storage: 'missing') }
@@ -424,8 +489,9 @@ RSpec.describe Project, factory_default: :keep do
end
include_context 'invalid urls'
+ include_context 'valid urls with CRLF'
- it 'does not allow urls with CR or LF characters' do
+ it 'does not allow URLs with unencoded CR or LF characters' do
project = build(:project)
aggregate_failures do
@@ -437,6 +503,19 @@ RSpec.describe Project, factory_default: :keep do
end
end
end
+
+ it 'allow URLs with CR or LF characters' do
+ project = build(:project)
+
+ aggregate_failures do
+ valid_urls_with_CRLF.each do |url|
+ project.import_url = url
+
+ expect(project).to be_valid
+ expect(project.errors).to be_empty
+ end
+ end
+ end
end
describe 'project pending deletion' do
@@ -1714,13 +1793,19 @@ RSpec.describe Project, factory_default: :keep do
allow(::Gitlab::ServiceDeskEmail).to receive(:config).and_return(config)
end
- it 'returns custom address when project_key is set' do
- create(:service_desk_setting, project: project, project_key: 'key1')
+ context 'when project_key is set' do
+ it 'returns custom address including the project_key' do
+ create(:service_desk_setting, project: project, project_key: 'key1')
- expect(subject).to eq("foo+#{project.full_path_slug}-key1@bar.com")
+ expect(subject).to eq("foo+#{project.full_path_slug}-key1@bar.com")
+ end
end
- it_behaves_like 'with incoming email address'
+ context 'when project_key is not set' do
+ it 'returns custom address including the project full path' do
+ expect(subject).to eq("foo+#{project.full_path_slug}-#{project.project_id}-issue-@bar.com")
+ end
+ end
end
end
@@ -1780,6 +1865,20 @@ RSpec.describe Project, factory_default: :keep do
end
end
+ describe '.without_integration' do
+ it 'returns projects without the integration' do
+ project_1, project_2, project_3, project_4 = create_list(:project, 4)
+ instance_integration = create(:jira_integration, :instance)
+ create(:jira_integration, project: project_1, inherit_from_id: instance_integration.id)
+ create(:jira_integration, project: project_2, inherit_from_id: nil)
+ create(:jira_integration, group: create(:group), project: nil, inherit_from_id: nil)
+ create(:jira_integration, project: project_3, inherit_from_id: nil)
+ create(:integrations_slack, project: project_4, inherit_from_id: nil)
+
+ expect(Project.without_integration(instance_integration)).to contain_exactly(project_4)
+ end
+ end
+
context 'repository storage by default' do
let(:project) { build(:project) }
diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb
index ead6238b2f4..5fbf1a9c502 100644
--- a/spec/models/project_statistics_spec.rb
+++ b/spec/models/project_statistics_spec.rb
@@ -325,12 +325,14 @@ RSpec.describe ProjectStatistics do
lfs_objects_size: 3,
snippets_size: 2,
pipeline_artifacts_size: 3,
+ build_artifacts_size: 3,
+ packages_size: 6,
uploads_size: 5
)
statistics.reload
- expect(statistics.storage_size).to eq 19
+ expect(statistics.storage_size).to eq 28
end
it 'works during wiki_size backfill' do
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index 8eab50abd8c..a6a56180ce1 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -234,6 +234,20 @@ RSpec.describe ProjectTeam do
expect(project.team.reporter?(user1)).to be(true)
expect(project.team.reporter?(user2)).to be(true)
end
+
+ context 'when `tasks_to_be_done` and `tasks_project_id` are passed' do
+ before do
+ stub_experiments(invite_members_for_task: true)
+ project.team.add_users([user1], :developer, tasks_to_be_done: %w(ci code), tasks_project_id: project.id)
+ end
+
+ it 'creates a member_task with the correct attributes', :aggregate_failures do
+ member = project.project_members.last
+
+ expect(member.tasks_to_be_done).to match_array([:ci, :code])
+ expect(member.member_task.project).to eq(project)
+ end
+ end
end
describe '#add_user' do
diff --git a/spec/models/protectable_dropdown_spec.rb b/spec/models/protectable_dropdown_spec.rb
index 918c3078405..ab3f455fe63 100644
--- a/spec/models/protectable_dropdown_spec.rb
+++ b/spec/models/protectable_dropdown_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe ProtectableDropdown do
describe '#protectable_ref_names' do
context 'when project repository is not empty' do
before do
- project.protected_branches.create(name: 'master')
+ create(:protected_branch, project: project, name: 'master')
end
it { expect(subject.protectable_ref_names).to include('feature') }
diff --git a/spec/models/redirect_route_spec.rb b/spec/models/redirect_route_spec.rb
index c6e35923b89..2de662fd4b4 100644
--- a/spec/models/redirect_route_spec.rb
+++ b/spec/models/redirect_route_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe RedirectRoute do
let(:group) { create(:group) }
- let!(:redirect_route) { group.redirect_routes.create(path: 'gitlabb') }
+ let!(:redirect_route) { group.redirect_routes.create!(path: 'gitlabb') }
describe 'relationships' do
it { is_expected.to belong_to(:source) }
@@ -17,10 +17,10 @@ RSpec.describe RedirectRoute do
end
describe '.matching_path_and_descendants' do
- let!(:redirect2) { group.redirect_routes.create(path: 'gitlabb/test') }
- let!(:redirect3) { group.redirect_routes.create(path: 'gitlabb/test/foo') }
- let!(:redirect4) { group.redirect_routes.create(path: 'gitlabb/test/foo/bar') }
- let!(:redirect5) { group.redirect_routes.create(path: 'gitlabb/test/baz') }
+ let!(:redirect2) { group.redirect_routes.create!(path: 'gitlabb/test') }
+ let!(:redirect3) { group.redirect_routes.create!(path: 'gitlabb/test/foo') }
+ let!(:redirect4) { group.redirect_routes.create!(path: 'gitlabb/test/foo/bar') }
+ let!(:redirect5) { group.redirect_routes.create!(path: 'gitlabb/test/baz') }
context 'when the redirect route matches with same casing' do
it 'returns correct routes' do
diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb
index b88813b3328..125fec61d72 100644
--- a/spec/models/release_spec.rb
+++ b/spec/models/release_spec.rb
@@ -26,10 +26,10 @@ RSpec.describe Release do
context 'when a release exists in the database without a name' do
it 'does not require name' do
existing_release_without_name = build(:release, project: project, author: user, name: nil)
- existing_release_without_name.save(validate: false)
+ existing_release_without_name.save!(validate: false)
existing_release_without_name.description = "change"
- existing_release_without_name.save
+ existing_release_without_name.save!
existing_release_without_name.reload
expect(existing_release_without_name).to be_valid
@@ -88,7 +88,7 @@ RSpec.describe Release do
describe '.create' do
it "fills released_at using created_at if it's not set" do
- release = described_class.create(project: project, author: user)
+ release = create(:release, project: project, author: user, released_at: nil)
expect(release.released_at).to eq(release.created_at)
end
@@ -96,14 +96,14 @@ RSpec.describe Release do
it "does not change released_at if it's set explicitly" do
released_at = Time.zone.parse('2018-10-20T18:00:00Z')
- release = described_class.create(project: project, author: user, released_at: released_at)
+ release = create(:release, project: project, author: user, released_at: released_at)
expect(release.released_at).to eq(released_at)
end
end
describe '#update' do
- subject { release.update(params) }
+ subject { release.update!(params) }
context 'when links do not exist' do
context 'when params are specified for creation' do
@@ -182,7 +182,7 @@ RSpec.describe Release do
it 'also deletes the associated evidence' do
release_with_evidence
- expect { release_with_evidence.destroy }.to change(Releases::Evidence, :count).by(-1)
+ expect { release_with_evidence.destroy! }.to change(Releases::Evidence, :count).by(-1)
end
end
end
@@ -190,7 +190,7 @@ RSpec.describe Release do
describe '#name' do
context 'name is nil' do
before do
- release.update(name: nil)
+ release.update!(name: nil)
end
it 'returns tag' do
diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb
index 382359ccb17..9f1d1c84da3 100644
--- a/spec/models/remote_mirror_spec.rb
+++ b/spec/models/remote_mirror_spec.rb
@@ -289,7 +289,7 @@ RSpec.describe RemoteMirror, :mailer do
context 'with remote mirroring disabled' do
it 'returns nil' do
- remote_mirror.update(enabled: false)
+ remote_mirror.update!(enabled: false)
expect(remote_mirror.sync).to be_nil
end
@@ -354,7 +354,7 @@ RSpec.describe RemoteMirror, :mailer do
let(:remote_mirror) { create(:project, :repository, :remote_mirror).remote_mirrors.first }
it 'resets all the columns when URL changes' do
- remote_mirror.update(last_error: Time.current,
+ remote_mirror.update!(last_error: Time.current,
last_update_at: Time.current,
last_successful_update_at: Time.current,
update_status: 'started',
@@ -378,7 +378,7 @@ RSpec.describe RemoteMirror, :mailer do
end
before do
- remote_mirror.update(last_update_started_at: Time.current)
+ remote_mirror.update!(last_update_started_at: Time.current)
end
context 'when remote mirror does not have status failed' do
@@ -393,7 +393,7 @@ RSpec.describe RemoteMirror, :mailer do
context 'when remote mirror has status failed' do
it 'returns false when last update started after the timestamp' do
- remote_mirror.update(update_status: 'failed')
+ remote_mirror.update!(update_status: 'failed')
expect(remote_mirror.updated_since?(timestamp)).to be false
end
@@ -409,7 +409,7 @@ RSpec.describe RemoteMirror, :mailer do
updated_at: 25.hours.ago)
project = mirror.project
project.pending_delete = true
- project.save
+ project.save!
mirror.reload
expect(mirror.sync).to be_nil
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 7bad907cf90..d50c60774b4 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -66,35 +66,58 @@ RSpec.describe Repository do
it { is_expected.not_to include('v1.0.0') }
end
- describe 'tags_sorted_by' do
+ describe '#tags_sorted_by' do
let(:tags_to_compare) { %w[v1.0.0 v1.1.0] }
- let(:feature_flag) { true }
-
- before do
- stub_feature_flags(tags_finder_gitaly: feature_flag)
- end
context 'name_desc' do
subject { repository.tags_sorted_by('name_desc').map(&:name) & tags_to_compare }
it { is_expected.to eq(['v1.1.0', 'v1.0.0']) }
-
- context 'when feature flag is disabled' do
- let(:feature_flag) { false }
-
- it { is_expected.to eq(['v1.1.0', 'v1.0.0']) }
- end
end
context 'name_asc' do
- subject { repository.tags_sorted_by('name_asc').map(&:name) & tags_to_compare }
+ subject { repository.tags_sorted_by('name_asc', pagination_params).map(&:name) & tags_to_compare }
+
+ let(:pagination_params) { nil }
it { is_expected.to eq(['v1.0.0', 'v1.1.0']) }
- context 'when feature flag is disabled' do
- let(:feature_flag) { false }
+ context 'with pagination' do
+ context 'with limit' do
+ let(:pagination_params) { { limit: 1 } }
+
+ it { is_expected.to eq(['v1.0.0']) }
+ end
+
+ context 'with page token and limit' do
+ let(:pagination_params) { { page_token: 'refs/tags/v1.0.0', limit: 1 } }
+
+ it { is_expected.to eq(['v1.1.0']) }
+ end
- it { is_expected.to eq(['v1.0.0', 'v1.1.0']) }
+ context 'with page token only' do
+ let(:pagination_params) { { page_token: 'refs/tags/v1.0.0' } }
+
+ it 'raises an ArgumentError' do
+ expect { subject }.to raise_error(ArgumentError)
+ end
+ end
+
+ context 'with negative limit' do
+ let(:pagination_params) { { limit: -1 } }
+
+ it 'returns all tags' do
+ is_expected.to eq(['v1.0.0', 'v1.1.0'])
+ end
+ end
+
+ context 'with unknown token' do
+ let(:pagination_params) { { page_token: 'unknown' } }
+
+ it 'raises an ArgumentError' do
+ expect { subject }.to raise_error(ArgumentError)
+ end
+ end
end
end
@@ -113,24 +136,12 @@ RSpec.describe Repository do
subject { repository.tags_sorted_by('updated_desc').map(&:name) & (tags_to_compare + [latest_tag]) }
it { is_expected.to eq([latest_tag, 'v1.1.0', 'v1.0.0']) }
-
- context 'when feature flag is disabled' do
- let(:feature_flag) { false }
-
- it { is_expected.to eq([latest_tag, 'v1.1.0', 'v1.0.0']) }
- end
end
context 'asc' do
subject { repository.tags_sorted_by('updated_asc').map(&:name) & (tags_to_compare + [latest_tag]) }
it { is_expected.to eq(['v1.0.0', 'v1.1.0', latest_tag]) }
-
- context 'when feature flag is disabled' do
- let(:feature_flag) { false }
-
- it { is_expected.to eq(['v1.0.0', 'v1.1.0', latest_tag]) }
- end
end
context 'annotated tag pointing to a blob' do
@@ -147,12 +158,6 @@ RSpec.describe Repository do
it { is_expected.to eq(['v1.0.0', 'v1.1.0', annotated_tag_name]) }
- context 'when feature flag is disabled' do
- let(:feature_flag) { false }
-
- it { is_expected.to eq(['v1.0.0', 'v1.1.0', annotated_tag_name]) }
- end
-
after do
rugged_repo(repository).tags.delete(annotated_tag_name)
end
@@ -163,12 +168,6 @@ RSpec.describe Repository do
subject { repository.tags_sorted_by('unknown_desc').map(&:name) & tags_to_compare }
it { is_expected.to eq(['v1.0.0', 'v1.1.0']) }
-
- context 'when feature flag is disabled' do
- let(:feature_flag) { false }
-
- it { is_expected.to eq(['v1.0.0', 'v1.1.0']) }
- end
end
end
diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb
index eb81db95cd3..b2fa9c24535 100644
--- a/spec/models/route_spec.rb
+++ b/spec/models/route_spec.rb
@@ -31,18 +31,18 @@ RSpec.describe Route do
context 'after update' do
it 'calls #create_redirect_for_old_path' do
expect(route).to receive(:create_redirect_for_old_path)
- route.update(path: 'foo')
+ route.update!(path: 'foo')
end
it 'calls #delete_conflicting_redirects' do
expect(route).to receive(:delete_conflicting_redirects)
- route.update(path: 'foo')
+ route.update!(path: 'foo')
end
end
context 'after create' do
it 'calls #delete_conflicting_redirects' do
- route.destroy
+ route.destroy!
new_route = described_class.new(source: group, path: group.path)
expect(new_route).to receive(:delete_conflicting_redirects)
new_route.save!
@@ -81,7 +81,7 @@ RSpec.describe Route do
context 'path update' do
context 'when route name is set' do
before do
- route.update(path: 'bar')
+ route.update!(path: 'bar')
end
it 'updates children routes with new path' do
@@ -111,7 +111,7 @@ RSpec.describe Route do
let!(:conflicting_redirect3) { route.create_redirect('gitlab-org') }
it 'deletes the conflicting redirects' do
- route.update(path: 'bar')
+ route.update!(path: 'bar')
expect(RedirectRoute.exists?(path: 'bar/test')).to be_falsey
expect(RedirectRoute.exists?(path: 'bar/test/foo')).to be_falsey
@@ -122,7 +122,7 @@ RSpec.describe Route do
context 'name update' do
it 'updates children routes with new path' do
- route.update(name: 'bar')
+ route.update!(name: 'bar')
expect(described_class.exists?(name: 'bar')).to be_truthy
expect(described_class.exists?(name: 'bar / test')).to be_truthy
@@ -134,7 +134,7 @@ RSpec.describe Route do
# Note: using `update_columns` to skip all validation and callbacks
route.update_columns(name: nil)
- expect { route.update(name: 'bar') }
+ expect { route.update!(name: 'bar') }
.to change { route.name }.from(nil).to('bar')
end
end
diff --git a/spec/models/sentry_issue_spec.rb b/spec/models/sentry_issue_spec.rb
index c24350d7067..09b23b6fd0d 100644
--- a/spec/models/sentry_issue_spec.rb
+++ b/spec/models/sentry_issue_spec.rb
@@ -53,7 +53,7 @@ RSpec.describe SentryIssue do
create(:sentry_issue)
project = sentry_issue.issue.project
sentry_issue_3 = build(:sentry_issue, issue: create(:issue, project: project), sentry_issue_identifier: sentry_issue.sentry_issue_identifier)
- sentry_issue_3.save(validate: false)
+ sentry_issue_3.save!(validate: false)
result = described_class.for_project_and_identifier(project, sentry_issue.sentry_issue_identifier)
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index 4e20a83f18e..e24dd910c39 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -98,7 +98,7 @@ RSpec.describe Snippet do
snippet = build(:snippet)
expect(snippet.statistics).to be_nil
- snippet.save
+ snippet.save!
expect(snippet.statistics).to be_persisted
end
@@ -289,7 +289,7 @@ RSpec.describe Snippet do
let(:access_level) { ProjectFeature::ENABLED }
before do
- project.project_feature.update(snippets_access_level: access_level)
+ project.project_feature.update!(snippets_access_level: access_level)
end
it 'includes snippets for projects with snippets enabled' do
@@ -623,7 +623,7 @@ RSpec.describe Snippet do
context 'when snippet_repository does not exist' do
it 'creates a snippet_repository' do
- snippet.snippet_repository.destroy
+ snippet.snippet_repository.destroy!
snippet.reload
expect do
diff --git a/spec/models/suggestion_spec.rb b/spec/models/suggestion_spec.rb
index 9a7624c253a..4f91908264f 100644
--- a/spec/models/suggestion_spec.rb
+++ b/spec/models/suggestion_spec.rb
@@ -154,6 +154,14 @@ RSpec.describe Suggestion do
it { is_expected.to eq("This suggestion already matches its content.") }
end
+ context 'when file is .ipynb' do
+ before do
+ allow(suggestion).to receive(:file_path).and_return("example.ipynb")
+ end
+
+ it { is_expected.to eq(_("This file was modified for readability, and can't accept suggestions. Edit it directly.")) }
+ end
+
context 'when applicable' do
it { is_expected.to be_nil }
end
diff --git a/spec/models/u2f_registration_spec.rb b/spec/models/u2f_registration_spec.rb
index aba2f27d104..7a70cf69566 100644
--- a/spec/models/u2f_registration_spec.rb
+++ b/spec/models/u2f_registration_spec.rb
@@ -5,9 +5,11 @@ require 'spec_helper'
RSpec.describe U2fRegistration do
let_it_be(:user) { create(:user) }
+ let(:u2f_registration_name) { 'u2f_device' }
+
let(:u2f_registration) do
device = U2F::FakeU2F.new(FFaker::BaconIpsum.characters(5))
- create(:u2f_registration, name: 'u2f_device',
+ create(:u2f_registration, name: u2f_registration_name,
user: user,
certificate: Base64.strict_encode64(device.cert_raw),
key_handle: U2F.urlsafe_encode64(device.key_handle_raw),
@@ -16,11 +18,27 @@ RSpec.describe U2fRegistration do
describe 'callbacks' do
describe '#create_webauthn_registration' do
- it 'creates webauthn registration' do
- u2f_registration.save!
+ shared_examples_for 'creates 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
+ end
+
+ it_behaves_like 'creates webauthn registration'
+
+ context 'when the u2f_registration has a blank name' do
+ let(:u2f_registration_name) { '' }
+
+ it_behaves_like 'creates webauthn registration'
+ end
+
+ context 'when the u2f_registration has the name as `nil`' do
+ let(:u2f_registration_name) { nil }
- webauthn_registration = WebauthnRegistration.where(u2f_registration_id: u2f_registration.id)
- expect(webauthn_registration).to exist
+ it_behaves_like 'creates webauthn registration'
end
it 'logs error' do
diff --git a/spec/models/upload_spec.rb b/spec/models/upload_spec.rb
index 0ac684cd04c..cdf73b203af 100644
--- a/spec/models/upload_spec.rb
+++ b/spec/models/upload_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe Upload do
it 'schedules checksum calculation' do
stub_const('UploadChecksumWorker', spy)
- upload = described_class.create(
+ upload = described_class.create!(
path: __FILE__,
size: described_class::CHECKSUM_THRESHOLD + 1.kilobyte,
model: build_stubbed(:user),
@@ -42,7 +42,7 @@ RSpec.describe Upload do
store: ObjectStorage::Store::LOCAL
)
- expect { upload.save }
+ expect { upload.save! }
.to change { upload.checksum }.from(nil)
.to(a_string_matching(/\A\h{64}\z/))
end
@@ -55,7 +55,7 @@ RSpec.describe Upload do
it 'calls delete_file!' do
is_expected.to receive(:delete_file!)
- subject.destroy
+ subject.destroy!
end
end
end
@@ -82,6 +82,18 @@ RSpec.describe Upload do
end
end
+ describe '#relative_path' do
+ it "delegates to the uploader's relative_path method" do
+ uploader = spy('FakeUploader')
+ upload = described_class.new(path: '/tmp/secret/file.jpg', store: ObjectStorage::Store::LOCAL)
+ expect(upload).to receive(:uploader_class).and_return(uploader)
+
+ upload.relative_path
+
+ expect(uploader).to have_received(:relative_path).with(upload)
+ end
+ end
+
describe '#calculate_checksum!' do
let(:upload) do
described_class.new(path: __FILE__,
diff --git a/spec/models/uploads/fog_spec.rb b/spec/models/uploads/fog_spec.rb
index 899e6f2064c..1ffe7c6c43b 100644
--- a/spec/models/uploads/fog_spec.rb
+++ b/spec/models/uploads/fog_spec.rb
@@ -40,7 +40,9 @@ RSpec.describe Uploads::Fog do
end
describe '#delete_keys' do
+ let(:connection) { ::Fog::Storage.new(FileUploader.object_store_credentials) }
let(:keys) { data_store.keys(relation) }
+ let(:paths) { relation.pluck(:path) }
let!(:uploads) { create_list(:upload, 2, :with_file, :issuable_upload, model: project) }
subject { data_store.delete_keys(keys) }
@@ -50,17 +52,32 @@ RSpec.describe Uploads::Fog do
end
it 'deletes multiple data' do
- paths = relation.pluck(:path)
+ paths.each do |path|
+ expect(connection.get_object('uploads', path)[:body]).not_to be_nil
+ end
+
+ subject
+
+ paths.each do |path|
+ expect { connection.get_object('uploads', path)[:body] }.to raise_error(Excon::Error::NotFound)
+ end
+ end
- ::Fog::Storage.new(FileUploader.object_store_credentials).tap do |connection|
+ context 'when one of keys is missing' do
+ let(:keys) { ['unknown'] + super() }
+
+ it 'deletes only existing keys' do
paths.each do |path|
expect(connection.get_object('uploads', path)[:body]).not_to be_nil
end
- end
- subject
+ expect_next_instance_of(::Fog::Storage) do |storage|
+ allow(storage).to receive(:delete_object).and_call_original
+ expect(storage).to receive(:delete_object).with('uploads', keys.first).and_raise(::Google::Apis::ClientError, 'NotFound')
+ end
+
+ subject
- ::Fog::Storage.new(FileUploader.object_store_credentials).tap do |connection|
paths.each do |path|
expect { connection.get_object('uploads', path)[:body] }.to raise_error(Excon::Error::NotFound)
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 21c5aea514a..b5d4614d206 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe User do
include ProjectForksHelper
include TermsHelper
include ExclusiveLeaseHelpers
+ include LdapHelpers
it_behaves_like 'having unique enum values'
@@ -98,7 +99,7 @@ RSpec.describe User do
it { is_expected.to have_many(:group_members) }
it { is_expected.to have_many(:groups) }
it { is_expected.to have_many(:keys).dependent(:destroy) }
- it { is_expected.to have_many(:expired_and_unnotified_keys) }
+ it { is_expected.to have_many(:expired_today_and_unnotified_keys) }
it { is_expected.to have_many(:deploy_keys).dependent(:nullify) }
it { is_expected.to have_many(:group_deploy_keys) }
it { is_expected.to have_many(:events).dependent(:delete_all) }
@@ -1123,7 +1124,7 @@ RSpec.describe User do
end
describe 'after commit hook' do
- describe '#update_emails_with_primary_email' do
+ describe 'when the primary email is updated' do
before do
@user = create(:user, email: 'primary@example.com').tap do |user|
user.skip_reconfirmation!
@@ -1132,13 +1133,7 @@ RSpec.describe User do
@user.reload
end
- it 'gets called when email updated' do
- expect(@user).to receive(:update_emails_with_primary_email)
-
- @user.update!(email: 'new_primary@example.com')
- end
-
- it 'adds old primary to secondary emails when secondary is a new email' do
+ it 'keeps old primary to secondary emails when secondary is a new email' do
@user.update!(email: 'new_primary@example.com')
@user.reload
@@ -1146,22 +1141,6 @@ RSpec.describe User do
expect(@user.emails.pluck(:email)).to match_array([@secondary.email, 'primary@example.com'])
end
- it 'adds old primary to secondary emails if secondary is becoming a primary' do
- @user.update!(email: @secondary.email)
- @user.reload
-
- expect(@user.emails.count).to eq 1
- expect(@user.emails.first.email).to eq 'primary@example.com'
- end
-
- it 'transfers old confirmation values into new secondary' do
- @user.update!(email: @secondary.email)
- @user.reload
-
- expect(@user.emails.count).to eq 1
- expect(@user.emails.first.confirmed_at).not_to eq nil
- end
-
context 'when the first email was unconfirmed and the second email gets confirmed' do
let(:user) { create(:user, :unconfirmed, email: 'should-be-unconfirmed@test.com') }
@@ -1178,11 +1157,8 @@ RSpec.describe User do
expect(user).to be_confirmed
end
- it 'keeps the unconfirmed email unconfirmed' do
- email = user.emails.first
-
- expect(email.email).to eq('should-be-unconfirmed@test.com')
- expect(email).not_to be_confirmed
+ it 'does not add unconfirmed email to secondary' do
+ expect(user.emails.map(&:email)).not_to include('should-be-unconfirmed@test.com')
end
it 'has only one email association' do
@@ -1244,7 +1220,7 @@ RSpec.describe User do
expect(user.email).to eq(confirmed_email)
end
- it 'moves the old email' do
+ it 'keeps the old email' do
email = user.reload.emails.first
expect(email.email).to eq(old_confirmed_email)
@@ -1499,7 +1475,7 @@ RSpec.describe User do
allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true)
end
- let(:user) { create(:user, confirmed_at: nil, unconfirmed_email: 'test@gitlab.com') }
+ let(:user) { create(:user, :unconfirmed, unconfirmed_email: 'test@gitlab.com') }
it 'returns unconfirmed' do
expect(user.confirmed?).to be_falsey
@@ -1509,6 +1485,22 @@ RSpec.describe User do
user.confirm
expect(user.confirmed?).to be_truthy
end
+
+ it 'adds the confirmed primary email to emails' do
+ expect(user.emails.confirmed.map(&:email)).not_to include(user.email)
+
+ user.confirm
+
+ expect(user.emails.confirmed.map(&:email)).to include(user.email)
+ end
+ end
+
+ context 'if the user is created with confirmed_at set to a time' do
+ let!(:user) { create(:user, email: 'test@gitlab.com', confirmed_at: Time.now.utc) }
+
+ it 'adds the confirmed primary email to emails upon creation' do
+ expect(user.emails.confirmed.map(&:email)).to include(user.email)
+ end
end
describe '#to_reference' do
@@ -2216,7 +2208,7 @@ RSpec.describe User do
end
context 'primary email not confirmed' do
- let(:user) { create(:user, confirmed_at: nil) }
+ let(:user) { create(:user, :unconfirmed) }
let!(:email) { create(:email, :confirmed, user: user, email: 'foo@example.com') }
it 'finds user respecting the confirmed flag' do
@@ -2231,7 +2223,7 @@ RSpec.describe User do
end
it 'returns nil when user is not confirmed' do
- user = create(:user, email: 'foo@example.com', confirmed_at: nil)
+ user = create(:user, :unconfirmed, email: 'foo@example.com')
expect(described_class.find_by_any_email(user.email, confirmed: false)).to eq(user)
expect(described_class.find_by_any_email(user.email, confirmed: true)).to be_nil
@@ -4155,6 +4147,23 @@ RSpec.describe User do
end
end
+ describe '#remove_project_authorizations' do
+ let_it_be(:project1) { create(:project) }
+ let_it_be(:project2) { create(:project) }
+ let_it_be(:project3) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ it 'removes the project authorizations of the user, in specified projects' do
+ create(:project_authorization, user: user, project: project1)
+ create(:project_authorization, user: user, project: project2)
+ create(:project_authorization, user: user, project: project3)
+
+ user.remove_project_authorizations([project1.id, project2.id])
+
+ expect(user.project_authorizations.pluck(:project_id)).to match_array([project3.id])
+ end
+ end
+
describe '#access_level=' do
let(:user) { build(:user) }
@@ -5817,7 +5826,7 @@ RSpec.describe User do
end
describe '#active_for_authentication?' do
- subject { user.active_for_authentication? }
+ subject(:active_for_authentication?) { user.active_for_authentication? }
let(:user) { create(:user) }
@@ -5827,6 +5836,14 @@ RSpec.describe User do
end
it { is_expected.to be false }
+
+ it 'does not check if LDAP is allowed' do
+ stub_ldap_setting(enabled: true)
+
+ expect(Gitlab::Auth::Ldap::Access).not_to receive(:allowed?)
+
+ active_for_authentication?
+ end
end
context 'when user is a ghost user' do
@@ -5837,6 +5854,28 @@ RSpec.describe User do
it { is_expected.to be false }
end
+ context 'when user is ldap_blocked' do
+ before do
+ user.ldap_block
+ end
+
+ it 'rechecks if LDAP is allowed when LDAP is enabled' do
+ stub_ldap_setting(enabled: true)
+
+ expect(Gitlab::Auth::Ldap::Access).to receive(:allowed?)
+
+ active_for_authentication?
+ end
+
+ it 'does not check if LDAP is allowed when LDAP is not enabled' do
+ stub_ldap_setting(enabled: false)
+
+ expect(Gitlab::Auth::Ldap::Access).not_to receive(:allowed?)
+
+ active_for_authentication?
+ end
+ end
+
context 'based on user type' do
using RSpec::Parameterized::TableSyntax
@@ -6011,7 +6050,7 @@ RSpec.describe User do
subject { user.confirmation_required_on_sign_in? }
context 'when user is confirmed' do
- let(:user) { build_stubbed(:user) }
+ let(:user) { create(:user) }
it 'is falsey' do
expect(user.confirmed?).to be_truthy
@@ -6203,4 +6242,31 @@ RSpec.describe User do
expect(described_class.get_ids_by_username([user_name])).to match_array([user_id])
end
end
+
+ describe 'user_project' do
+ it 'returns users project matched by username and public visibility' do
+ user = create(:user)
+ public_project = create(:project, :public, path: user.username, namespace: user.namespace)
+ create(:project, namespace: user.namespace)
+
+ expect(user.user_project).to eq(public_project)
+ end
+ end
+
+ describe 'user_readme' do
+ it 'returns readme from user project' do
+ user = create(:user)
+ create(:project, :repository, :public, path: user.username, namespace: user.namespace)
+
+ expect(user.user_readme.name).to eq('README.md')
+ expect(user.user_readme.data).to include('testme')
+ end
+
+ it 'returns nil if project is private' do
+ user = create(:user)
+ create(:project, :repository, :private, path: user.username, namespace: user.namespace)
+
+ expect(user.user_readme).to be(nil)
+ end
+ end
end
diff --git a/spec/models/users/credit_card_validation_spec.rb b/spec/models/users/credit_card_validation_spec.rb
index d2b4f5ebd65..43edf7ed093 100644
--- a/spec/models/users/credit_card_validation_spec.rb
+++ b/spec/models/users/credit_card_validation_spec.rb
@@ -6,19 +6,25 @@ RSpec.describe Users::CreditCardValidation do
it { is_expected.to belong_to(:user) }
it { is_expected.to validate_length_of(:holder_name).is_at_most(26) }
+ it { is_expected.to validate_length_of(:network).is_at_most(32) }
it { is_expected.to validate_numericality_of(:last_digits).is_less_than_or_equal_to(9999) }
describe '.similar_records' do
- let(:card_details) { subject.attributes.slice(:expiration_date, :last_digits, :holder_name) }
+ let(:card_details) do
+ subject.attributes.with_indifferent_access.slice(:expiration_date, :last_digits, :network, :holder_name)
+ end
- subject(:credit_card_validation) { create(:credit_card_validation) }
+ subject!(:credit_card_validation) { create(:credit_card_validation, holder_name: 'Alice') }
let!(:match1) { create(:credit_card_validation, card_details) }
- let!(:other1) { create(:credit_card_validation, card_details.merge(last_digits: 9)) }
- let!(:match2) { create(:credit_card_validation, card_details) }
- let!(:other2) { create(:credit_card_validation, card_details.merge(holder_name: 'foo bar')) }
+ let!(:match2) { create(:credit_card_validation, card_details.merge(holder_name: 'Bob')) }
+ let!(:non_match1) { create(:credit_card_validation, card_details.merge(last_digits: 9)) }
+ let!(:non_match2) { create(:credit_card_validation, card_details.merge(network: 'unknown')) }
+ let!(:non_match3) do
+ create(:credit_card_validation, card_details.dup.tap { |h| h[:expiration_date] += 1.year })
+ end
- it 'returns records with matching credit card, ordered by credit_card_validated_at' do
+ it 'returns matches with the same last_digits, expiration and network, ordered by credit_card_validated_at' do
expect(subject.similar_records).to eq([match2, match1, subject])
end
end
diff --git a/spec/models/users/in_product_marketing_email_spec.rb b/spec/models/users/in_product_marketing_email_spec.rb
index a9ddd86677c..cf08cf7ceed 100644
--- a/spec/models/users/in_product_marketing_email_spec.rb
+++ b/spec/models/users/in_product_marketing_email_spec.rb
@@ -21,7 +21,8 @@ RSpec.describe Users::InProductMarketingEmail, type: :model do
describe '.tracks' do
it 'has an entry for every track' do
- expect(Namespaces::InProductMarketingEmailsService::TRACKS.keys).to match_array(described_class.tracks.keys.map(&:to_sym))
+ tracks = [Namespaces::InviteTeamEmailService::TRACK, Namespaces::InProductMarketingEmailsService::TRACKS.keys].flatten
+ expect(tracks).to match_array(described_class.tracks.keys.map(&:to_sym))
end
end
diff --git a/spec/models/users/merge_request_interaction_spec.rb b/spec/models/users/merge_request_interaction_spec.rb
index d333577fa1a..12c7fa43a60 100644
--- a/spec/models/users/merge_request_interaction_spec.rb
+++ b/spec/models/users/merge_request_interaction_spec.rb
@@ -61,7 +61,7 @@ RSpec.describe ::Users::MergeRequestInteraction do
merge_request.reviewers << user
end
- it { is_expected.to eq(Types::MergeRequestReviewStateEnum.values['UNREVIEWED'].value) }
+ it { is_expected.to eq(Types::MergeRequestReviewStateEnum.values['ATTENTION_REQUESTED'].value) }
it 'implies not reviewed' do
expect(interaction).not_to be_reviewed
@@ -70,7 +70,8 @@ RSpec.describe ::Users::MergeRequestInteraction do
context 'when the user has provided a review' do
before do
- merge_request.merge_request_reviewers.create!(reviewer: user, state: MergeRequestReviewer.states['reviewed'])
+ reviewer = merge_request.merge_request_reviewers.create!(reviewer: user)
+ reviewer.update!(state: MergeRequestReviewer.states['reviewed'])
end
it { is_expected.to eq(Types::MergeRequestReviewStateEnum.values['REVIEWED'].value) }
diff --git a/spec/models/users_statistics_spec.rb b/spec/models/users_statistics_spec.rb
index b4b7ddb7c63..8553d0bfdb0 100644
--- a/spec/models/users_statistics_spec.rb
+++ b/spec/models/users_statistics_spec.rb
@@ -34,11 +34,11 @@ RSpec.describe UsersStatistics do
describe '.create_current_stats!' do
before do
- create_list(:user_highest_role, 4)
+ create_list(:user_highest_role, 1)
create_list(:user_highest_role, 2, :guest)
- create_list(:user_highest_role, 3, :reporter)
- create_list(:user_highest_role, 4, :developer)
- create_list(:user_highest_role, 3, :maintainer)
+ create_list(:user_highest_role, 2, :reporter)
+ create_list(:user_highest_role, 2, :developer)
+ create_list(:user_highest_role, 2, :maintainer)
create_list(:user_highest_role, 2, :owner)
create_list(:user, 2, :bot)
create_list(:user, 1, :blocked)
@@ -49,11 +49,11 @@ RSpec.describe UsersStatistics do
context 'when successful' do
it 'creates an entry with the current statistics values' do
expect(described_class.create_current_stats!).to have_attributes(
- without_groups_and_projects: 4,
+ without_groups_and_projects: 1,
with_highest_role_guest: 2,
- with_highest_role_reporter: 3,
- with_highest_role_developer: 4,
- with_highest_role_maintainer: 3,
+ with_highest_role_reporter: 2,
+ with_highest_role_developer: 2,
+ with_highest_role_maintainer: 2,
with_highest_role_owner: 2,
bots: 2,
blocked: 1
diff --git a/spec/models/webauthn_registration_spec.rb b/spec/models/webauthn_registration_spec.rb
new file mode 100644
index 00000000000..6813854bf6c
--- /dev/null
+++ b/spec/models/webauthn_registration_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WebauthnRegistration do
+ describe 'relations' do
+ it { is_expected.to belong_to(:user) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:credential_xid) }
+ it { is_expected.to validate_presence_of(:public_key) }
+ it { is_expected.to validate_presence_of(:counter) }
+ it { is_expected.to validate_length_of(:name).is_at_least(0) }
+ it { is_expected.not_to allow_value(nil).for(:name) }
+ it do
+ is_expected.to validate_numericality_of(:counter)
+ .only_integer
+ .is_greater_than_or_equal_to(0)
+ .is_less_than_or_equal_to(4294967295)
+ end
+ end
+end
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index 201ccf0fc14..fc4fbace790 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -11,8 +11,8 @@ RSpec.describe GroupPolicy do
it do
expect_allowed(:read_group)
- expect_allowed(:read_organization)
- expect_allowed(:read_contact)
+ expect_allowed(:read_crm_organization)
+ expect_allowed(:read_crm_contact)
expect_allowed(:read_counts)
expect_allowed(*read_group_permissions)
expect_disallowed(:upload_file)
@@ -33,8 +33,8 @@ RSpec.describe GroupPolicy do
end
it { expect_disallowed(:read_group) }
- it { expect_disallowed(:read_organization) }
- it { expect_disallowed(:read_contact) }
+ it { expect_disallowed(:read_crm_organization) }
+ it { expect_disallowed(:read_crm_contact) }
it { expect_disallowed(:read_counts) }
it { expect_disallowed(*read_group_permissions) }
end
@@ -48,8 +48,8 @@ RSpec.describe GroupPolicy do
end
it { expect_disallowed(:read_group) }
- it { expect_disallowed(:read_organization) }
- it { expect_disallowed(:read_contact) }
+ it { expect_disallowed(:read_crm_organization) }
+ it { expect_disallowed(:read_crm_contact) }
it { expect_disallowed(:read_counts) }
it { expect_disallowed(*read_group_permissions) }
end
@@ -933,8 +933,8 @@ RSpec.describe GroupPolicy do
it { is_expected.to be_allowed(:read_package) }
it { is_expected.to be_allowed(:read_group) }
- it { is_expected.to be_allowed(:read_organization) }
- it { is_expected.to be_allowed(:read_contact) }
+ it { is_expected.to be_allowed(:read_crm_organization) }
+ it { is_expected.to be_allowed(:read_crm_contact) }
it { is_expected.to be_disallowed(:create_package) }
end
@@ -944,8 +944,8 @@ RSpec.describe GroupPolicy do
it { is_expected.to be_allowed(:create_package) }
it { is_expected.to be_allowed(:read_package) }
it { is_expected.to be_allowed(:read_group) }
- it { is_expected.to be_allowed(:read_organization) }
- it { is_expected.to be_allowed(:read_contact) }
+ it { is_expected.to be_allowed(:read_crm_organization) }
+ it { is_expected.to be_allowed(:read_crm_contact) }
it { is_expected.to be_disallowed(:destroy_package) }
end
@@ -1032,4 +1032,17 @@ RSpec.describe GroupPolicy do
it { is_expected.to be_disallowed(:update_runners_registration_token) }
end
end
+
+ context 'with customer_relations feature flag disabled' do
+ let(:current_user) { owner }
+
+ before do
+ stub_feature_flags(customer_relations: false)
+ end
+
+ it { is_expected.to be_disallowed(:read_crm_contact) }
+ it { is_expected.to be_disallowed(:read_crm_organization) }
+ it { is_expected.to be_disallowed(:admin_crm_contact) }
+ it { is_expected.to be_disallowed(:admin_crm_organization) }
+ end
end
diff --git a/spec/policies/namespaces/project_namespace_policy_spec.rb b/spec/policies/namespaces/project_namespace_policy_spec.rb
index 22f3ccec1f8..5bb38deb498 100644
--- a/spec/policies/namespaces/project_namespace_policy_spec.rb
+++ b/spec/policies/namespaces/project_namespace_policy_spec.rb
@@ -4,7 +4,8 @@ require 'spec_helper'
RSpec.describe NamespacePolicy do
let_it_be(:parent) { create(:namespace) }
- let_it_be(:namespace) { create(:project_namespace, parent: parent) }
+ let_it_be(:project) { create(:project, namespace: parent) }
+ let_it_be(:namespace) { project.project_namespace }
let(:permissions) do
[:owner_access, :create_projects, :admin_namespace, :read_namespace,
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index f36b0a62aa3..2953c198af6 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -104,29 +104,71 @@ RSpec.describe ProjectPolicy do
end
context 'pipeline feature' do
- let(:project) { private_project }
+ let(:project) { private_project }
+ let(:current_user) { developer }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
- before do
- private_project.add_developer(current_user)
+ describe 'for confirmed user' do
+ it 'allows modify pipelines' do
+ expect_allowed(:create_pipeline)
+ expect_allowed(:update_pipeline)
+ expect_allowed(:create_pipeline_schedule)
+ end
end
describe 'for unconfirmed user' do
- let(:current_user) { create(:user, confirmed_at: nil) }
+ let(:current_user) { project.owner.tap { |u| u.update!(confirmed_at: nil) } }
it 'disallows to modify pipelines' do
expect_disallowed(:create_pipeline)
expect_disallowed(:update_pipeline)
+ expect_disallowed(:destroy_pipeline)
expect_disallowed(:create_pipeline_schedule)
end
end
- describe 'for confirmed user' do
- let(:current_user) { developer }
+ describe 'destroy permission' do
+ describe 'for developers' do
+ it 'prevents :destroy_pipeline' do
+ expect(current_user.can?(:destroy_pipeline, pipeline)).to be_falsey
+ end
+ end
- it 'allows modify pipelines' do
- expect_allowed(:create_pipeline)
- expect_allowed(:update_pipeline)
- expect_allowed(:create_pipeline_schedule)
+ describe 'for maintainers' do
+ let(:current_user) { maintainer }
+
+ it 'prevents :destroy_pipeline' do
+ project.add_maintainer(maintainer)
+ expect(current_user.can?(:destroy_pipeline, pipeline)).to be_falsey
+ end
+ end
+
+ describe 'for project owner' do
+ let(:current_user) { project.owner }
+
+ it 'allows :destroy_pipeline' do
+ expect(current_user.can?(:destroy_pipeline, pipeline)).to be_truthy
+ end
+
+ context 'on archived projects' do
+ before do
+ project.update!(archived: true)
+ end
+
+ it 'prevents :destroy_pipeline' do
+ expect(current_user.can?(:destroy_pipeline, pipeline)).to be_falsey
+ end
+ end
+
+ context 'on archived pending_delete projects' do
+ before do
+ project.update!(archived: true, pending_delete: true)
+ end
+
+ it 'allows :destroy_pipeline' do
+ expect(current_user.can?(:destroy_pipeline, pipeline)).to be_truthy
+ end
+ end
end
end
end
@@ -955,6 +997,28 @@ RSpec.describe ProjectPolicy do
end
end
+ context 'infrastructure google cloud feature' do
+ %w(guest reporter developer).each do |role|
+ context role do
+ let(:current_user) { send(role) }
+
+ it 'disallows managing google cloud' do
+ expect_disallowed(:admin_project_google_cloud)
+ end
+ end
+ end
+
+ %w(maintainer owner).each do |role|
+ context role do
+ let(:current_user) { send(role) }
+
+ it 'allows managing google cloud' do
+ expect_allowed(:admin_project_google_cloud)
+ end
+ end
+ end
+ end
+
describe 'design permissions' do
include DesignManagementTestHelpers
diff --git a/spec/presenters/award_emoji_presenter_spec.rb b/spec/presenters/award_emoji_presenter_spec.rb
index 58ee985f165..a23196282a2 100644
--- a/spec/presenters/award_emoji_presenter_spec.rb
+++ b/spec/presenters/award_emoji_presenter_spec.rb
@@ -6,21 +6,22 @@ RSpec.describe AwardEmojiPresenter do
let(:emoji_name) { 'thumbsup' }
let(:award_emoji) { build(:award_emoji, name: emoji_name) }
let(:presenter) { described_class.new(award_emoji) }
+ let(:emoji) { TanukiEmoji.find_by_alpha_code(emoji_name) }
describe '#description' do
- it { expect(presenter.description).to eq Gitlab::Emoji.emojis[emoji_name]['description'] }
+ it { expect(presenter.description).to eq emoji.description }
end
describe '#unicode' do
- it { expect(presenter.unicode).to eq Gitlab::Emoji.emojis[emoji_name]['unicode'] }
+ it { expect(presenter.unicode).to eq emoji.hex }
end
describe '#unicode_version' do
- it { expect(presenter.unicode_version).to eq Gitlab::Emoji.emoji_unicode_version(emoji_name) }
+ it { expect(presenter.unicode_version).to eq('6.0') }
end
describe '#emoji' do
- it { expect(presenter.emoji).to eq Gitlab::Emoji.emojis[emoji_name]['moji'] }
+ it { expect(presenter.emoji).to eq emoji.codepoints }
end
describe 'when presenting an award emoji with an invalid name' do
diff --git a/spec/presenters/blob_presenter_spec.rb b/spec/presenters/blob_presenter_spec.rb
index 466a2b55e76..28e18708eab 100644
--- a/spec/presenters/blob_presenter_spec.rb
+++ b/spec/presenters/blob_presenter_spec.rb
@@ -31,6 +31,20 @@ RSpec.describe BlobPresenter do
it { expect(presenter.replace_path).to eq("/#{project.full_path}/-/create/#{blob.commit_id}/#{blob.path}") }
end
+ describe '#pipeline_editor_path' do
+ context 'when blob is .gitlab-ci.yml' do
+ before do
+ project.repository.create_file(user, '.gitlab-ci.yml', '',
+ message: 'Add a ci file',
+ branch_name: 'main')
+ end
+
+ let(:blob) { repository.blob_at('main', '.gitlab-ci.yml') }
+
+ it { expect(presenter.pipeline_editor_path).to eq("/#{project.full_path}/-/ci/editor?branch_name=#{blob.commit_id}") }
+ end
+ end
+
describe '#ide_edit_path' do
it { expect(presenter.ide_edit_path).to eq("/-/ide/project/#{project.full_path}/edit/HEAD/-/files/ruby/regex.rb") }
end
@@ -121,6 +135,47 @@ RSpec.describe BlobPresenter do
end
end
+ describe '#highlight_transformed' do
+ context 'when blob is ipynb' do
+ let(:blob) { repository.blob_at('f6b7a707', 'files/ipython/markdown-table.ipynb') }
+ let(:git_blob) { blob.__getobj__ }
+
+ before do
+ allow(git_blob).to receive(:transformed_for_diff).and_return(true)
+ end
+
+ it 'uses md as the transformed language' do
+ expect(Gitlab::Highlight).to receive(:highlight).with('files/ipython/markdown-table.ipynb', anything, plain: nil, language: 'md')
+
+ presenter.highlight_transformed
+ end
+
+ it 'transforms the blob' do
+ expect(Gitlab::Highlight).to receive(:highlight).with('files/ipython/markdown-table.ipynb', include("%%"), plain: nil, language: 'md')
+
+ presenter.highlight_transformed
+ end
+ end
+
+ context 'when blob is other file type' do
+ let(:git_blob) { blob.__getobj__ }
+
+ before do
+ allow(git_blob)
+ .to receive(:data)
+ .and_return("line one\nline two\nline 3")
+
+ allow(blob).to receive(:language_from_gitattributes).and_return('ruby')
+ end
+
+ it 'does not transform the file' do
+ expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: nil, language: 'ruby')
+
+ presenter.highlight_transformed
+ end
+ end
+ end
+
describe '#raw_plain_data' do
let(:blob) { repository.blob_at('HEAD', file) }
diff --git a/spec/presenters/ci/build_runner_presenter_spec.rb b/spec/presenters/ci/build_runner_presenter_spec.rb
index 4422773fec6..b8d0b093a24 100644
--- a/spec/presenters/ci/build_runner_presenter_spec.rb
+++ b/spec/presenters/ci/build_runner_presenter_spec.rb
@@ -259,12 +259,7 @@ RSpec.describe Ci::BuildRunnerPresenter do
describe '#runner_variables' do
subject { presenter.runner_variables }
- let_it_be(:project_with_flag_disabled) { create(:project, :repository) }
- let_it_be(:project_with_flag_enabled) { create(:project, :repository) }
-
- before do
- stub_feature_flags(variable_inside_variable: [project_with_flag_enabled])
- end
+ let_it_be(:project) { create(:project, :repository) }
shared_examples 'returns an array with the expected variables' do
it 'returns an array' do
@@ -276,21 +271,11 @@ RSpec.describe Ci::BuildRunnerPresenter do
end
end
- context 'when FF :variable_inside_variable is disabled' do
- let(:sha) { project_with_flag_disabled.repository.commit.sha }
- let(:pipeline) { create(:ci_pipeline, sha: sha, project: project_with_flag_disabled) }
- let(:build) { create(:ci_build, pipeline: pipeline) }
-
- it_behaves_like 'returns an array with the expected variables'
- end
+ let(:sha) { project.repository.commit.sha }
+ let(:pipeline) { create(:ci_pipeline, sha: sha, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
- context 'when FF :variable_inside_variable is enabled' do
- let(:sha) { project_with_flag_enabled.repository.commit.sha }
- let(:pipeline) { create(:ci_pipeline, sha: sha, project: project_with_flag_enabled) }
- let(:build) { create(:ci_build, pipeline: pipeline) }
-
- it_behaves_like 'returns an array with the expected variables'
- end
+ it_behaves_like 'returns an array with the expected variables'
end
describe '#runner_variables subset' do
@@ -305,32 +290,12 @@ RSpec.describe Ci::BuildRunnerPresenter do
create(:ci_pipeline_variable, key: 'C', value: 'value', pipeline: build.pipeline)
end
- context 'when FF :variable_inside_variable is disabled' do
- before do
- stub_feature_flags(variable_inside_variable: false)
- end
-
- it 'returns non-expanded variables' do
- is_expected.to eq [
- { key: 'A', value: 'refA-$B', public: false, masked: false },
- { key: 'B', value: 'refB-$C-$D', public: false, masked: false },
- { key: 'C', value: 'value', public: false, masked: false }
- ]
- end
- end
-
- context 'when FF :variable_inside_variable is enabled' do
- before do
- stub_feature_flags(variable_inside_variable: [build.project])
- end
-
- it 'returns expanded and sorted variables' do
- is_expected.to eq [
- { key: 'C', value: 'value', public: false, masked: false },
- { key: 'B', value: 'refB-value-$D', public: false, masked: false },
- { key: 'A', value: 'refA-refB-value-$D', public: false, masked: false }
- ]
- end
+ it 'returns expanded and sorted variables' do
+ is_expected.to eq [
+ { key: 'C', value: 'value', public: false, masked: false },
+ { key: 'B', value: 'refB-value-$D', public: false, masked: false },
+ { key: 'A', value: 'refA-refB-value-$D', public: false, masked: false }
+ ]
end
end
end
diff --git a/spec/presenters/packages/npm/package_presenter_spec.rb b/spec/presenters/packages/npm/package_presenter_spec.rb
index 65f69d4056b..49046492ab4 100644
--- a/spec/presenters/packages/npm/package_presenter_spec.rb
+++ b/spec/presenters/packages/npm/package_presenter_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe ::Packages::Npm::PackagePresenter do
+ using RSpec::Parameterized::TableSyntax
+
let_it_be(:project) { create(:project) }
let_it_be(:package_name) { "@#{project.root_namespace.path}/test" }
let_it_be(:package1) { create(:npm_package, version: '2.0.4', project: project, name: package_name) }
@@ -13,42 +15,88 @@ RSpec.describe ::Packages::Npm::PackagePresenter do
let(:presenter) { described_class.new(package_name, packages) }
describe '#versions' do
- subject { presenter.versions }
+ let_it_be('package_json') do
+ {
+ 'name': package_name,
+ 'version': '2.0.4',
+ 'deprecated': 'warning!',
+ 'bin': './cli.js',
+ 'directories': ['lib'],
+ 'engines': { 'npm': '^7.5.6' },
+ '_hasShrinkwrap': false,
+ 'dist': {
+ 'tarball': 'http://localhost/tarball.tgz',
+ 'shasum': '1234567890'
+ },
+ 'custom_field': 'foo_bar'
+ }
+ end
- context 'for packages without dependencies' do
- it { is_expected.to be_a(Hash) }
- it { expect(subject[package1.version].with_indifferent_access).to match_schema('public_api/v4/packages/npm_package_version') }
- it { expect(subject[package2.version].with_indifferent_access).to match_schema('public_api/v4/packages/npm_package_version') }
+ let(:presenter) { described_class.new(package_name, packages, include_metadata: include_metadata) }
- ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type|
- it { expect(subject.dig(package1.version, dependency_type)).to be nil }
- it { expect(subject.dig(package2.version, dependency_type)).to be nil }
- end
+ subject { presenter.versions }
- it 'avoids N+1 database queries' do
- check_n_plus_one(:versions) do
- create_list(:npm_package, 5, project: project, name: package_name)
+ where(:has_dependencies, :has_metadatum, :include_metadata) do
+ true | true | true
+ false | true | true
+ true | false | true
+ false | false | true
+
+ # TODO : to remove along with packages_npm_abbreviated_metadata
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/344827
+ true | true | false
+ false | true | false
+ true | false | false
+ false | false | false
+ end
+
+ with_them do
+ if params[:has_dependencies]
+ ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type|
+ let_it_be("package_dependency_link_for_#{dependency_type}") { create(:packages_dependency_link, package: package1, dependency_type: dependency_type) }
end
end
- end
- context 'for packages with dependencies' do
- ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type|
- let_it_be("package_dependency_link_for_#{dependency_type}") { create(:packages_dependency_link, package: package1, dependency_type: dependency_type) }
+ if params[:has_metadatum]
+ let_it_be('package_metadatadum') { create(:npm_metadatum, package: package1, package_json: package_json) }
end
it { is_expected.to be_a(Hash) }
it { expect(subject[package1.version].with_indifferent_access).to match_schema('public_api/v4/packages/npm_package_version') }
it { expect(subject[package2.version].with_indifferent_access).to match_schema('public_api/v4/packages/npm_package_version') }
- ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type|
- it { expect(subject.dig(package1.version, dependency_type.to_s)).to be_any }
+ it { expect(subject[package1.version]['custom_field']).to be_blank }
+
+ context 'dependencies' do
+ ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type|
+ if params[:has_dependencies]
+ it { expect(subject.dig(package1.version, dependency_type.to_s)).to be_any }
+ else
+ it { expect(subject.dig(package1.version, dependency_type)).to be nil }
+ end
+
+ it { expect(subject.dig(package2.version, dependency_type)).to be nil }
+ end
+ end
+
+ context 'metadatum' do
+ ::Packages::Npm::PackagePresenter::PACKAGE_JSON_ALLOWED_FIELDS.each do |metadata_field|
+ if params[:has_metadatum] && params[:include_metadata]
+ it { expect(subject.dig(package1.version, metadata_field)).not_to be nil }
+ else
+ it { expect(subject.dig(package1.version, metadata_field)).to be nil }
+ end
+
+ it { expect(subject.dig(package2.version, metadata_field)).to be nil }
+ end
end
it 'avoids N+1 database queries' do
check_n_plus_one(:versions) do
create_list(:npm_package, 5, project: project, name: package_name).each do |npm_package|
- ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type|
- create(:packages_dependency_link, package: npm_package, dependency_type: dependency_type)
+ if has_dependencies
+ ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type|
+ create(:packages_dependency_link, package: npm_package, dependency_type: dependency_type)
+ end
end
end
end
diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb
index 5f789f59908..27b777dec5f 100644
--- a/spec/presenters/project_presenter_spec.rb
+++ b/spec/presenters/project_presenter_spec.rb
@@ -567,44 +567,27 @@ RSpec.describe ProjectPresenter do
end
describe '#upload_anchor_data' do
- context 'with empty_repo_upload enabled' do
+ context 'when a user can push to the default branch' do
before do
- stub_experiments(empty_repo_upload: :candidate)
- end
-
- context 'user can push to branch' do
- before do
- project.add_developer(user)
- end
-
- it 'returns upload_anchor_data' do
- expect(presenter.upload_anchor_data).to have_attributes(
- is_link: false,
- label: a_string_including('Upload file'),
- data: {
- "can_push_code" => "true",
- "original_branch" => "master",
- "path" => "/#{project.full_path}/-/create/master",
- "project_path" => project.full_path,
- "target_branch" => "master"
- }
- )
- end
+ project.add_developer(user)
end
- context 'user cannot push to branch' do
- it 'returns nil' do
- expect(presenter.upload_anchor_data).to be_nil
- end
+ it 'returns upload_anchor_data' do
+ expect(presenter.upload_anchor_data).to have_attributes(
+ is_link: false,
+ label: a_string_including('Upload file'),
+ data: {
+ "can_push_code" => "true",
+ "original_branch" => "master",
+ "path" => "/#{project.full_path}/-/create/master",
+ "project_path" => project.full_path,
+ "target_branch" => "master"
+ }
+ )
end
end
- context 'with empty_repo_upload disabled' do
- before do
- stub_experiments(empty_repo_upload: :control)
- project.add_developer(user)
- end
-
+ context 'when the user cannot push to default branch' do
it 'returns nil' do
expect(presenter.upload_anchor_data).to be_nil
end
@@ -666,7 +649,6 @@ RSpec.describe ProjectPresenter do
context 'for a developer' do
before do
project.add_developer(user)
- stub_experiments(empty_repo_upload: :candidate)
end
it 'orders the items correctly' do
@@ -680,16 +662,6 @@ RSpec.describe ProjectPresenter do
a_string_including('CI/CD')
)
end
-
- context 'when not in the upload experiment' do
- before do
- stub_experiments(empty_repo_upload: :control)
- end
-
- it 'does not include upload button' do
- expect(empty_repo_statistics_buttons.map(&:label)).not_to start_with(a_string_including('Upload'))
- end
- end
end
end
@@ -781,20 +753,4 @@ RSpec.describe ProjectPresenter do
it { is_expected.to match(/code_quality_walkthrough=true.*template=Code-Quality/) }
end
-
- describe 'empty_repo_upload_experiment?' do
- subject { presenter.empty_repo_upload_experiment? }
-
- it 'returns false when upload_anchor_data is nil' do
- allow(presenter).to receive(:upload_anchor_data).and_return(nil)
-
- expect(subject).to be false
- end
-
- it 'returns true when upload_anchor_data exists' do
- allow(presenter).to receive(:upload_anchor_data).and_return(true)
-
- expect(subject).to be true
- end
- end
end
diff --git a/spec/presenters/release_presenter_spec.rb b/spec/presenters/release_presenter_spec.rb
index b2e7b684644..925a69ca92d 100644
--- a/spec/presenters/release_presenter_spec.rb
+++ b/spec/presenters/release_presenter_spec.rb
@@ -63,12 +63,6 @@ RSpec.describe ReleasePresenter do
it 'returns its own url' do
is_expected.to eq(project_release_url(project, release))
end
-
- context 'when user is guest' do
- let(:user) { guest }
-
- it { is_expected.to be_nil }
- end
end
describe '#opened_merge_requests_url' do
@@ -147,13 +141,5 @@ RSpec.describe ReleasePresenter do
it 'returns the release name' do
is_expected.to eq release.name
end
-
- context "when a user is not allowed to access any repository information" do
- let(:presenter) { described_class.new(release, current_user: guest) }
-
- it 'returns a replacement name to avoid potentially leaking tag information' do
- is_expected.to eq "Release-#{release.id}"
- end
- end
end
end
diff --git a/spec/requests/admin/applications_controller_spec.rb b/spec/requests/admin/applications_controller_spec.rb
new file mode 100644
index 00000000000..03553757080
--- /dev/null
+++ b/spec/requests/admin/applications_controller_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Admin::ApplicationsController, :enable_admin_mode do
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:application) { create(:oauth_application, owner_id: nil, owner_type: nil) }
+ let_it_be(:show_path) { admin_application_path(application) }
+ let_it_be(:create_path) { admin_applications_path }
+
+ before do
+ sign_in(admin)
+ end
+
+ include_examples 'applications controller - GET #show'
+
+ include_examples 'applications controller - POST #create'
+end
diff --git a/spec/requests/api/api_spec.rb b/spec/requests/api/api_spec.rb
index 95eb503c6bc..6a02f81fcae 100644
--- a/spec/requests/api/api_spec.rb
+++ b/spec/requests/api/api_spec.rb
@@ -116,7 +116,7 @@ RSpec.describe API::API do
'meta.root_namespace' => project.namespace.full_path,
'meta.user' => user.username,
'meta.client_id' => a_string_matching(%r{\Auser/.+}),
- 'meta.feature_category' => 'issue_tracking',
+ 'meta.feature_category' => 'team_planning',
'route' => '/api/:version/projects/:id/issues')
end
@@ -200,6 +200,28 @@ RSpec.describe API::API do
expect(response).to have_gitlab_http_status(:not_found)
end
end
+
+ context 'when there is an unhandled exception for an anonymous request' do
+ it 'logs all application context fields and the route' do
+ expect(described_class::LOG_FORMATTER).to receive(:call) do |_severity, _datetime, _, data|
+ expect(data.stringify_keys)
+ .to include('correlation_id' => an_instance_of(String),
+ 'meta.caller_id' => 'GET /api/:version/broadcast_messages',
+ 'meta.remote_ip' => an_instance_of(String),
+ 'meta.client_id' => a_string_matching(%r{\Aip/.+}),
+ 'meta.feature_category' => 'navigation',
+ 'route' => '/api/:version/broadcast_messages')
+
+ expect(data.stringify_keys).not_to include('meta.project', 'meta.root_namespace', 'meta.user')
+ end
+
+ expect(BroadcastMessage).to receive(:all).and_raise('An error!')
+
+ get(api('/broadcast_messages'))
+
+ expect(response).to have_gitlab_http_status(:internal_server_error)
+ end
+ end
end
describe 'Marginalia comments' do
diff --git a/spec/requests/api/ci/jobs_spec.rb b/spec/requests/api/ci/jobs_spec.rb
index b6ab9310471..410020b68cd 100644
--- a/spec/requests/api/ci/jobs_spec.rb
+++ b/spec/requests/api/ci/jobs_spec.rb
@@ -176,6 +176,111 @@ RSpec.describe API::Ci::Jobs do
end
end
+ describe 'GET /job/allowed_agents' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:group_agent) { create(:cluster_agent, project: create(:project, group: group)) }
+ let_it_be(:group_authorization) { create(:agent_group_authorization, agent: group_agent, group: group) }
+ let_it_be(:project_agent) { create(:cluster_agent, project: project) }
+
+ before(:all) do
+ project.update!(group: group_authorization.group)
+ end
+
+ let(:implicit_authorization) { Clusters::Agents::ImplicitAuthorization.new(agent: project_agent) }
+
+ let(:headers) { { API::Ci::Helpers::Runner::JOB_TOKEN_HEADER => job.token } }
+ let(:job) { create(:ci_build, :artifacts, pipeline: pipeline, user: api_user, status: job_status) }
+ let(:job_status) { 'running' }
+ let(:params) { {} }
+
+ subject do
+ get api('/job/allowed_agents'), headers: headers, params: params
+ end
+
+ before do
+ subject
+ end
+
+ context 'when token is valid and user is authorized' do
+ shared_examples_for 'valid allowed_agents request' do
+ it 'returns agent info', :aggregate_failures do
+ expect(response).to have_gitlab_http_status(:ok)
+
+ expect(json_response.dig('job', 'id')).to eq(job.id)
+ expect(json_response.dig('pipeline', 'id')).to eq(job.pipeline_id)
+ expect(json_response.dig('project', 'id')).to eq(job.project_id)
+ expect(json_response.dig('project', 'groups')).to match_array([{ 'id' => group_authorization.group.id }])
+ expect(json_response.dig('user', 'id')).to eq(api_user.id)
+ expect(json_response.dig('user', 'username')).to eq(api_user.username)
+ expect(json_response.dig('user', 'roles_in_project')).to match_array %w(guest reporter developer)
+ expect(json_response).not_to include('environment')
+ expect(json_response['allowed_agents']).to match_array([
+ {
+ 'id' => implicit_authorization.agent_id,
+ 'config_project' => hash_including('id' => implicit_authorization.agent.project_id),
+ 'configuration' => implicit_authorization.config
+ },
+ {
+ 'id' => group_authorization.agent_id,
+ 'config_project' => hash_including('id' => group_authorization.agent.project_id),
+ 'configuration' => group_authorization.config
+ }
+ ])
+ end
+ end
+
+ it_behaves_like 'valid allowed_agents request'
+
+ context 'when deployment' do
+ let(:job) { create(:ci_build, :artifacts, :with_deployment, environment: 'production', pipeline: pipeline, user: api_user, status: job_status) }
+
+ it 'includes environment slug' do
+ expect(json_response.dig('environment', 'slug')).to eq('production')
+ end
+ end
+
+ context 'when passing the token as params' do
+ let(:headers) { {} }
+ let(:params) { { job_token: job.token } }
+
+ it_behaves_like 'valid allowed_agents request'
+ end
+ end
+
+ context 'when user is anonymous' do
+ let(:api_user) { nil }
+
+ it 'returns unauthorized' do
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'when token is invalid because job has finished' do
+ let(:job_status) { 'success' }
+
+ it 'returns unauthorized' do
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'when token is invalid' do
+ let(:headers) { { API::Ci::Helpers::Runner::JOB_TOKEN_HEADER => 'bad_token' } }
+
+ it 'returns unauthorized' do
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'when token is valid but not CI_JOB_TOKEN' do
+ let(:token) { create(:personal_access_token, user: user) }
+ let(:headers) { { 'Private-Token' => token.token } }
+
+ it 'returns not found' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
describe 'GET /projects/:id/jobs' do
let(:query) { {} }
@@ -203,6 +308,7 @@ RSpec.describe API::Ci::Jobs do
it 'returns no artifacts nor trace data' do
json_job = json_response.first
+ expect(response).to have_gitlab_http_status(:ok)
expect(json_job['artifacts_file']).to be_nil
expect(json_job['artifacts']).to be_an Array
expect(json_job['artifacts']).to be_empty
@@ -321,6 +427,22 @@ RSpec.describe API::Ci::Jobs do
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
+
+ context 'when trace artifact record exists with no stored file', :skip_before_request do
+ before do
+ create(:ci_job_artifact, :unarchived_trace_artifact, job: job, project: job.project)
+ end
+
+ it 'returns no artifacts nor trace data' do
+ get api("/projects/#{project.id}/jobs/#{job.id}", api_user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['artifacts']).to be_an Array
+ expect(json_response['artifacts'].size).to eq(1)
+ expect(json_response['artifacts'][0]['file_type']).to eq('trace')
+ expect(json_response['artifacts'][0]['filename']).to eq('job.log')
+ end
+ end
end
describe 'DELETE /projects/:id/jobs/:job_id/artifacts' do
@@ -456,6 +578,7 @@ RSpec.describe API::Ci::Jobs do
expect(response.headers.to_h)
.to include('Content-Type' => 'application/json',
'Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
+ expect(response.parsed_body).to be_empty
end
context 'when artifacts are locked' do
@@ -826,6 +949,7 @@ RSpec.describe API::Ci::Jobs do
expect(response.headers.to_h)
.to include('Content-Type' => 'application/json',
'Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
+ expect(response.parsed_body).to be_empty
end
end
@@ -919,7 +1043,16 @@ RSpec.describe API::Ci::Jobs do
end
end
- context 'when trace is file' do
+ context 'when live trace and uploadless trace artifact' do
+ let(:job) { create(:ci_build, :trace_live, :unarchived_trace_artifact, pipeline: pipeline) }
+
+ it 'returns specific job trace' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to eq(job.trace.raw)
+ end
+ end
+
+ context 'when trace is live' do
let(:job) { create(:ci_build, :trace_live, pipeline: pipeline) }
it 'returns specific job trace' do
@@ -927,6 +1060,28 @@ RSpec.describe API::Ci::Jobs do
expect(response.body).to eq(job.trace.raw)
end
end
+
+ context 'when no trace' do
+ let(:job) { create(:ci_build, pipeline: pipeline) }
+
+ it 'returns empty trace' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to be_empty
+ end
+ end
+
+ context 'when trace artifact record exists with no stored file' do
+ let(:job) { create(:ci_build, pipeline: pipeline) }
+
+ before do
+ create(:ci_job_artifact, :unarchived_trace_artifact, job: job, project: job.project)
+ end
+
+ it 'returns empty trace' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to be_empty
+ end
+ end
end
context 'unauthorized user' do
@@ -1038,9 +1193,7 @@ RSpec.describe API::Ci::Jobs do
post api("/projects/#{project.id}/jobs/#{job.id}/erase", user)
end
- context 'job is erasable' do
- let(:job) { create(:ci_build, :trace_artifact, :artifacts, :test_reports, :success, project: project, pipeline: pipeline) }
-
+ shared_examples_for 'erases job' do
it 'erases job content' do
expect(response).to have_gitlab_http_status(:created)
expect(job.job_artifacts.count).to eq(0)
@@ -1049,6 +1202,12 @@ RSpec.describe API::Ci::Jobs do
expect(job.artifacts_metadata.present?).to be_falsy
expect(job.has_job_artifacts?).to be_falsy
end
+ end
+
+ context 'job is erasable' do
+ let(:job) { create(:ci_build, :trace_artifact, :artifacts, :test_reports, :success, project: project, pipeline: pipeline) }
+
+ it_behaves_like 'erases job'
it 'updates job' do
job.reload
@@ -1058,6 +1217,12 @@ RSpec.describe API::Ci::Jobs do
end
end
+ context 'when job has an unarchived trace artifact' do
+ let(:job) { create(:ci_build, :success, :trace_live, :unarchived_trace_artifact, project: project, pipeline: pipeline) }
+
+ it_behaves_like 'erases job'
+ end
+
context 'job is not erasable' do
let(:job) { create(:ci_build, :trace_live, project: project, pipeline: pipeline) }
diff --git a/spec/requests/api/ci/runner/jobs_request_post_spec.rb b/spec/requests/api/ci/runner/jobs_request_post_spec.rb
index c3fbef9be48..fdf1a278d4c 100644
--- a/spec/requests/api/ci/runner/jobs_request_post_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_request_post_spec.rb
@@ -218,9 +218,11 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
expect(json_response['git_info']).to eq(expected_git_info)
expect(json_response['image']).to eq({ 'name' => 'ruby:2.7', 'entrypoint' => '/bin/sh', 'ports' => [] })
expect(json_response['services']).to eq([{ 'name' => 'postgres', 'entrypoint' => nil,
- 'alias' => nil, 'command' => nil, 'ports' => [] },
+ 'alias' => nil, 'command' => nil, 'ports' => [], 'variables' => nil },
{ 'name' => 'docker:stable-dind', 'entrypoint' => '/bin/sh',
- 'alias' => 'docker', 'command' => 'sleep 30', 'ports' => [] }])
+ 'alias' => 'docker', 'command' => 'sleep 30', 'ports' => [], 'variables' => [] },
+ { 'name' => 'mysql:latest', 'entrypoint' => nil,
+ 'alias' => nil, 'command' => nil, 'ports' => [], 'variables' => [{ 'key' => 'MYSQL_ROOT_PASSWORD', 'value' => 'root123.' }] }])
expect(json_response['steps']).to eq(expected_steps)
expect(json_response['artifacts']).to eq(expected_artifacts)
expect(json_response['cache']).to eq(expected_cache)
diff --git a/spec/requests/api/debian_group_packages_spec.rb b/spec/requests/api/debian_group_packages_spec.rb
index 3e11b480860..d881d4350fb 100644
--- a/spec/requests/api/debian_group_packages_spec.rb
+++ b/spec/requests/api/debian_group_packages_spec.rb
@@ -9,35 +9,36 @@ RSpec.describe API::DebianGroupPackages do
context 'with invalid parameter' do
let(:url) { "/groups/1/-/packages/debian/dists/with+space/InRelease" }
- it_behaves_like 'Debian repository GET request', :bad_request, /^distribution is invalid$/
+ it_behaves_like 'Debian packages GET request', :bad_request, /^distribution is invalid$/
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/Release.gpg' do
let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/Release.gpg" }
- it_behaves_like 'Debian repository read endpoint', 'GET request', :success, /^-----BEGIN PGP SIGNATURE-----/
+ it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^-----BEGIN PGP SIGNATURE-----/
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/Release' do
let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/Release" }
- it_behaves_like 'Debian repository read endpoint', 'GET request', :success, /^Codename: fixture-distribution\n$/
+ it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Codename: fixture-distribution\n$/
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/InRelease' do
let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/InRelease" }
- it_behaves_like 'Debian repository read endpoint', 'GET request', :success, /^-----BEGIN PGP SIGNED MESSAGE-----/
+ it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^-----BEGIN PGP SIGNED MESSAGE-----/
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/Packages' do
let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/Packages" }
- it_behaves_like 'Debian repository read endpoint', 'GET request', :success, /Description: This is an incomplete Packages file/
+ it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Packages file/
end
describe 'GET groups/:id/-/packages/debian/pool/:codename/:project_id/:letter/:package_name/:package_version/:file_name' do
let(:url) { "/groups/#{container.id}/-/packages/debian/pool/#{package.debian_distribution.codename}/#{project.id}/#{letter}/#{package.name}/#{package.version}/#{file_name}" }
+ let(:file_name) { params[:file_name] }
using RSpec::Parameterized::TableSyntax
@@ -51,9 +52,7 @@ RSpec.describe API::DebianGroupPackages do
end
with_them do
- include_context 'with file_name', params[:file_name]
-
- it_behaves_like 'Debian repository read endpoint', 'GET request', :success, params[:success_body]
+ it_behaves_like 'Debian packages read endpoint', 'GET', :success, params[:success_body]
end
end
end
diff --git a/spec/requests/api/debian_project_packages_spec.rb b/spec/requests/api/debian_project_packages_spec.rb
index d0b0debaf13..bd68bf912e1 100644
--- a/spec/requests/api/debian_project_packages_spec.rb
+++ b/spec/requests/api/debian_project_packages_spec.rb
@@ -9,35 +9,36 @@ RSpec.describe API::DebianProjectPackages do
context 'with invalid parameter' do
let(:url) { "/projects/1/packages/debian/dists/with+space/InRelease" }
- it_behaves_like 'Debian repository GET request', :bad_request, /^distribution is invalid$/
+ it_behaves_like 'Debian packages GET request', :bad_request, /^distribution is invalid$/
end
describe 'GET projects/:id/packages/debian/dists/*distribution/Release.gpg' do
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/Release.gpg" }
- it_behaves_like 'Debian repository read endpoint', 'GET request', :success, /^-----BEGIN PGP SIGNATURE-----/
+ it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^-----BEGIN PGP SIGNATURE-----/
end
describe 'GET projects/:id/packages/debian/dists/*distribution/Release' do
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/Release" }
- it_behaves_like 'Debian repository read endpoint', 'GET request', :success, /^Codename: fixture-distribution\n$/
+ it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Codename: fixture-distribution\n$/
end
describe 'GET projects/:id/packages/debian/dists/*distribution/InRelease' do
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/InRelease" }
- it_behaves_like 'Debian repository read endpoint', 'GET request', :success, /^-----BEGIN PGP SIGNED MESSAGE-----/
+ it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^-----BEGIN PGP SIGNED MESSAGE-----/
end
describe 'GET projects/:id/packages/debian/dists/*distribution/:component/binary-:architecture/Packages' do
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/Packages" }
- it_behaves_like 'Debian repository read endpoint', 'GET request', :success, /Description: This is an incomplete Packages file/
+ it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Packages file/
end
describe 'GET projects/:id/packages/debian/pool/:codename/:letter/:package_name/:package_version/:file_name' do
let(:url) { "/projects/#{container.id}/packages/debian/pool/#{package.debian_distribution.codename}/#{letter}/#{package.name}/#{package.version}/#{file_name}" }
+ let(:file_name) { params[:file_name] }
using RSpec::Parameterized::TableSyntax
@@ -51,9 +52,7 @@ RSpec.describe API::DebianProjectPackages do
end
with_them do
- include_context 'with file_name', params[:file_name]
-
- it_behaves_like 'Debian repository read endpoint', 'GET request', :success, params[:success_body]
+ it_behaves_like 'Debian packages read endpoint', 'GET', :success, params[:success_body]
end
end
@@ -65,13 +64,13 @@ RSpec.describe API::DebianProjectPackages do
context 'with a deb' do
let(:file_name) { 'libsample0_1.2.3~alpha2_amd64.deb' }
- it_behaves_like 'Debian repository write endpoint', 'upload request', :created
+ it_behaves_like 'Debian packages write endpoint', 'upload', :created, nil
end
context 'with a changes file' do
let(:file_name) { 'sample_1.2.3~alpha2_amd64.changes' }
- it_behaves_like 'Debian repository write endpoint', 'upload request', :created
+ it_behaves_like 'Debian packages write endpoint', 'upload', :created, nil
end
end
@@ -80,7 +79,7 @@ RSpec.describe API::DebianProjectPackages do
let(:method) { :put }
let(:url) { "/projects/#{container.id}/packages/debian/#{file_name}/authorize" }
- it_behaves_like 'Debian repository write endpoint', 'upload authorize request', :created
+ it_behaves_like 'Debian packages write endpoint', 'upload authorize', :created, nil
end
end
end
diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb
index a01c66a311c..1daa7c38e04 100644
--- a/spec/requests/api/deploy_keys_spec.rb
+++ b/spec/requests/api/deploy_keys_spec.rb
@@ -8,8 +8,9 @@ RSpec.describe API::DeployKeys do
let_it_be(:admin) { create(:admin) }
let_it_be(:project) { create(:project, creator_id: user.id) }
let_it_be(:project2) { create(:project, creator_id: user.id) }
-
- let(:deploy_key) { create(:deploy_key, public: true) }
+ let_it_be(:project3) { create(:project, creator_id: user.id) }
+ let_it_be(:deploy_key) { create(:deploy_key, public: true) }
+ let_it_be(:deploy_key_private) { create(:deploy_key, public: false) }
let!(:deploy_keys_project) do
create(:deploy_keys_project, project: project, deploy_key: deploy_key)
@@ -33,13 +34,56 @@ RSpec.describe API::DeployKeys do
end
context 'when authenticated as admin' do
+ let_it_be(:pat) { create(:personal_access_token, user: admin) }
+
+ def make_api_request(params = {})
+ get api('/deploy_keys', personal_access_token: pat), params: params
+ end
+
it 'returns all deploy keys' do
- get api('/deploy_keys', admin)
+ make_api_request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
+ expect(response).to match_response_schema('public_api/v4/deploy_keys')
expect(json_response).to be_an Array
- expect(json_response.first['id']).to eq(deploy_keys_project.deploy_key.id)
+
+ expect(json_response[0]['id']).to eq(deploy_key.id)
+ expect(json_response[1]['id']).to eq(deploy_key_private.id)
+ end
+
+ it 'avoids N+1 database queries', :use_sql_query_cache, :request_store do
+ create(:deploy_keys_project, :write_access, project: project2, deploy_key: deploy_key)
+
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) { make_api_request }
+
+ deploy_key2 = create(:deploy_key, public: true)
+ create(:deploy_keys_project, :write_access, project: project3, deploy_key: deploy_key2)
+
+ expect { make_api_request }.not_to exceed_all_query_limit(control)
+ end
+
+ context 'when `public` parameter is `true`' do
+ it 'only returns public deploy keys' do
+ make_api_request({ public: true })
+
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(deploy_key.id)
+ end
+ end
+
+ context 'projects_with_write_access' do
+ let!(:deploy_keys_project2) { create(:deploy_keys_project, :write_access, project: project2, deploy_key: deploy_key) }
+ let!(:deploy_keys_project3) { create(:deploy_keys_project, :write_access, project: project3, deploy_key: deploy_key) }
+
+ it 'returns projects with write access' do
+ make_api_request
+
+ response_projects_with_write_access = json_response.first['projects_with_write_access']
+
+ expect(response_projects_with_write_access[0]['id']).to eq(project2.id)
+ expect(response_projects_with_write_access[1]['id']).to eq(project3.id)
+ end
end
end
end
@@ -58,6 +102,7 @@ RSpec.describe API::DeployKeys do
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(deploy_key.title)
+ expect(json_response.first).not_to have_key(:projects_with_write_access)
end
it 'returns multiple deploy keys without N + 1' do
@@ -77,6 +122,7 @@ RSpec.describe API::DeployKeys do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['title']).to eq(deploy_key.title)
+ expect(json_response).not_to have_key(:projects_with_write_access)
end
it 'returns 404 Not Found with invalid ID' do
diff --git a/spec/requests/api/error_tracking/collector_spec.rb b/spec/requests/api/error_tracking/collector_spec.rb
index 7acadeb1287..21e2849fef0 100644
--- a/spec/requests/api/error_tracking/collector_spec.rb
+++ b/spec/requests/api/error_tracking/collector_spec.rb
@@ -24,10 +24,10 @@ RSpec.describe API::ErrorTracking::Collector do
end
RSpec.shared_examples 'successful request' do
- it 'writes to the database and returns no content' do
+ it 'writes to the database and returns OK' do
expect { subject }.to change { ErrorTracking::ErrorEvent.count }.by(1)
- expect(response).to have_gitlab_http_status(:no_content)
+ expect(response).to have_gitlab_http_status(:ok)
end
end
@@ -89,13 +89,27 @@ RSpec.describe API::ErrorTracking::Collector do
context 'transaction request type' do
let(:params) { fixture_file('error_tracking/transaction.txt') }
- it 'does nothing and returns no content' do
+ it 'does nothing and returns ok' do
expect { subject }.not_to change { ErrorTracking::ErrorEvent.count }
- expect(response).to have_gitlab_http_status(:no_content)
+ expect(response).to have_gitlab_http_status(:ok)
end
end
+ context 'gzip body' do
+ let(:headers) do
+ {
+ 'X-Sentry-Auth' => "Sentry sentry_key=#{client_key.public_key}",
+ 'HTTP_CONTENT_ENCODING' => 'gzip',
+ 'CONTENT_TYPE' => 'application/x-sentry-envelope'
+ }
+ end
+
+ let(:params) { ActiveSupport::Gzip.compress(raw_event) }
+
+ it_behaves_like 'successful request'
+ end
+
it_behaves_like 'successful request'
end
@@ -122,6 +136,35 @@ RSpec.describe API::ErrorTracking::Collector do
it_behaves_like 'bad request'
end
+ context 'body with string instead of json' do
+ let(:params) { '"********"' }
+
+ it_behaves_like 'bad request'
+ end
+
+ context 'collector fails with validation error' do
+ before do
+ allow(::ErrorTracking::CollectErrorService)
+ .to receive(:new).and_raise(ActiveRecord::RecordInvalid)
+ end
+
+ it_behaves_like 'bad request'
+ end
+
+ context 'gzip body' do
+ let(:headers) do
+ {
+ 'X-Sentry-Auth' => "Sentry sentry_key=#{client_key.public_key}",
+ 'HTTP_CONTENT_ENCODING' => 'gzip',
+ 'CONTENT_TYPE' => 'application/json'
+ }
+ end
+
+ let(:params) { ActiveSupport::Gzip.compress(raw_event) }
+
+ it_behaves_like 'successful request'
+ end
+
context 'sentry_key as param and empty headers' do
let(:url) { "/error_tracking/collector/api/#{project.id}/store?sentry_key=#{sentry_key}" }
let(:headers) { {} }
diff --git a/spec/requests/api/features_spec.rb b/spec/requests/api/features_spec.rb
index 0e163ec2154..35dba93b766 100644
--- a/spec/requests/api/features_spec.rb
+++ b/spec/requests/api/features_spec.rb
@@ -256,6 +256,21 @@ RSpec.describe API::Features, stub_feature_flags: false do
)
end
+ it 'creates a feature with the given percentage of time if passed a float' do
+ post api("/features/#{feature_name}", admin), params: { value: '0.01' }
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response).to match(
+ 'name' => feature_name,
+ 'state' => 'conditional',
+ 'gates' => [
+ { 'key' => 'boolean', 'value' => false },
+ { 'key' => 'percentage_of_time', 'value' => 0.01 }
+ ],
+ 'definition' => known_feature_flag_definition_hash
+ )
+ end
+
it 'creates a feature with the given percentage of actors if passed an integer' do
post api("/features/#{feature_name}", admin), params: { value: '50', key: 'percentage_of_actors' }
@@ -270,6 +285,21 @@ RSpec.describe API::Features, stub_feature_flags: false do
'definition' => known_feature_flag_definition_hash
)
end
+
+ it 'creates a feature with the given percentage of actors if passed a float' do
+ post api("/features/#{feature_name}", admin), params: { value: '0.01', key: 'percentage_of_actors' }
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response).to match(
+ 'name' => feature_name,
+ 'state' => 'conditional',
+ 'gates' => [
+ { 'key' => 'boolean', 'value' => false },
+ { 'key' => 'percentage_of_actors', 'value' => 0.01 }
+ ],
+ 'definition' => known_feature_flag_definition_hash
+ )
+ end
end
context 'when the feature exists' do
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index 0b898496dd6..6aa12b6ff48 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -47,6 +47,15 @@ RSpec.describe API::Files do
"/projects/#{project.id}/repository/files/#{file_path}"
end
+ def expect_to_send_git_blob(url, params)
+ expect(Gitlab::Workhorse).to receive(:send_git_blob)
+
+ get url, params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.parsed_body).to be_empty
+ end
+
context 'http headers' do
it 'converts value into string' do
helper.set_http_headers(test: 1)
@@ -257,11 +266,7 @@ RSpec.describe API::Files do
it 'returns raw file info' do
url = route(file_path) + "/raw"
- expect(Gitlab::Workhorse).to receive(:send_git_blob)
-
- get api(url, api_user, **options), params: params
-
- expect(response).to have_gitlab_http_status(:ok)
+ expect_to_send_git_blob(api(url, api_user, **options), params)
expect(headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
end
@@ -523,11 +528,8 @@ RSpec.describe API::Files do
it 'returns raw file info' do
url = route(file_path) + "/raw"
- expect(Gitlab::Workhorse).to receive(:send_git_blob)
- get api(url, current_user), params: params
-
- expect(response).to have_gitlab_http_status(:ok)
+ expect_to_send_git_blob(api(url, current_user), params)
end
context 'when ref is not provided' do
@@ -537,39 +539,29 @@ RSpec.describe API::Files do
it 'returns response :ok', :aggregate_failures do
url = route(file_path) + "/raw"
- expect(Gitlab::Workhorse).to receive(:send_git_blob)
- get api(url, current_user), params: {}
-
- expect(response).to have_gitlab_http_status(:ok)
+ expect_to_send_git_blob(api(url, current_user), {})
end
end
it 'returns raw file info for files with dots' do
url = route('.gitignore') + "/raw"
- expect(Gitlab::Workhorse).to receive(:send_git_blob)
- get api(url, current_user), params: params
-
- expect(response).to have_gitlab_http_status(:ok)
+ expect_to_send_git_blob(api(url, current_user), params)
end
it 'returns file by commit sha' do
# This file is deleted on HEAD
file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee"
params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
- expect(Gitlab::Workhorse).to receive(:send_git_blob)
- get api(route(file_path) + "/raw", current_user), params: params
-
- expect(response).to have_gitlab_http_status(:ok)
+ expect_to_send_git_blob(api(route(file_path) + "/raw", current_user), params)
end
it 'sets no-cache headers' do
url = route('.gitignore') + "/raw"
- expect(Gitlab::Workhorse).to receive(:send_git_blob)
- get api(url, current_user), params: params
+ expect_to_send_git_blob(api(url, current_user), params)
expect(response.headers["Cache-Control"]).to eq("max-age=0, private, must-revalidate, no-store, no-cache")
expect(response.headers["Pragma"]).to eq("no-cache")
@@ -633,11 +625,9 @@ RSpec.describe API::Files do
# This file is deleted on HEAD
file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee"
params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
- expect(Gitlab::Workhorse).to receive(:send_git_blob)
+ url = api(route(file_path) + "/raw", personal_access_token: token)
- get api(route(file_path) + "/raw", personal_access_token: token), params: params
-
- expect(response).to have_gitlab_http_status(:ok)
+ expect_to_send_git_blob(url, params)
end
end
end
diff --git a/spec/requests/api/generic_packages_spec.rb b/spec/requests/api/generic_packages_spec.rb
index 7e439a22e4b..2d85d7b9583 100644
--- a/spec/requests/api/generic_packages_spec.rb
+++ b/spec/requests/api/generic_packages_spec.rb
@@ -297,6 +297,37 @@ RSpec.describe API::GenericPackages do
end
end
+ context 'with select' do
+ context 'with a valid value' do
+ context 'package_file' do
+ let(:params) { super().merge(select: 'package_file') }
+
+ it 'returns a package file' do
+ headers = workhorse_headers.merge(auth_header)
+
+ upload_file(params, headers)
+
+ aggregate_failures do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to have_key('id')
+ end
+ end
+ end
+ end
+
+ context 'with an invalid value' do
+ let(:params) { super().merge(select: 'invalid_value') }
+
+ it 'returns a package file' do
+ headers = workhorse_headers.merge(auth_header)
+
+ upload_file(params, headers)
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+ end
+
context 'with a status' do
context 'valid status' do
let(:params) { super().merge(status: 'hidden') }
diff --git a/spec/requests/api/graphql/ci/pipelines_spec.rb b/spec/requests/api/graphql/ci/pipelines_spec.rb
index 6587061094d..1f47f678898 100644
--- a/spec/requests/api/graphql/ci/pipelines_spec.rb
+++ b/spec/requests/api/graphql/ci/pipelines_spec.rb
@@ -186,6 +186,69 @@ RSpec.describe 'Query.project(fullPath).pipelines' do
end
end
+ describe '.job_artifacts' do
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:pipeline_job_1) { create(:ci_build, pipeline: pipeline, name: 'Job 1') }
+ let_it_be(:pipeline_job_artifact_1) { create(:ci_job_artifact, job: pipeline_job_1) }
+ let_it_be(:pipeline_job_2) { create(:ci_build, pipeline: pipeline, name: 'Job 2') }
+ let_it_be(:pipeline_job_artifact_2) { create(:ci_job_artifact, job: pipeline_job_2) }
+
+ let(:path) { %i[project pipelines nodes jobArtifacts] }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ pipelines {
+ nodes {
+ jobArtifacts {
+ name
+ downloadPath
+ fileType
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ before do
+ post_graphql(query, current_user: user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns the job_artifacts of a pipeline' do
+ job_artifacts_graphql_data = graphql_data_at(*path).flatten
+
+ expect(
+ job_artifacts_graphql_data.map { |pip| pip['name'] }
+ ).to contain_exactly(pipeline_job_artifact_1.filename, pipeline_job_artifact_2.filename)
+ end
+
+ it 'avoids N+1 queries' do
+ first_user = create(:user)
+ second_user = create(:user)
+
+ control_count = ActiveRecord::QueryRecorder.new do
+ post_graphql(query, current_user: first_user)
+ end
+
+ pipeline_2 = create(:ci_pipeline, project: project)
+ pipeline_2_job_1 = create(:ci_build, pipeline: pipeline_2, name: 'Pipeline 2 Job 1')
+ create(:ci_job_artifact, job: pipeline_2_job_1)
+ pipeline_2_job_2 = create(:ci_build, pipeline: pipeline_2, name: 'Pipeline 2 Job 2')
+ create(:ci_job_artifact, job: pipeline_2_job_2)
+
+ expect do
+ post_graphql(query, current_user: second_user)
+ end.not_to exceed_query_limit(control_count)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
describe '.jobs(securityReportTypes)' do
let_it_be(:query) do
%(
diff --git a/spec/requests/api/graphql/gitlab_schema_spec.rb b/spec/requests/api/graphql/gitlab_schema_spec.rb
index b41d851439b..8bbeae97f57 100644
--- a/spec/requests/api/graphql/gitlab_schema_spec.rb
+++ b/spec/requests/api/graphql/gitlab_schema_spec.rb
@@ -190,19 +190,18 @@ RSpec.describe 'GitlabSchema configurations' do
let(:query) { File.read(Rails.root.join('spec/fixtures/api/graphql/introspection.graphql')) }
it 'logs the query complexity and depth' do
- analyzer_memo = {
- query_string: query,
- variables: {}.to_s,
- complexity: 181,
- depth: 13,
- duration_s: 7,
- operation_name: 'IntrospectionQuery',
- used_fields: an_instance_of(Array),
- used_deprecated_fields: an_instance_of(Array)
- }
-
expect_any_instance_of(Gitlab::Graphql::QueryAnalyzers::LoggerAnalyzer).to receive(:duration).and_return(7)
- expect(Gitlab::GraphqlLogger).to receive(:info).with(analyzer_memo)
+
+ expect(Gitlab::GraphqlLogger).to receive(:info).with(
+ hash_including(
+ trace_type: 'execute_query',
+ "query_analysis.duration_s" => 7,
+ "query_analysis.complexity" => 181,
+ "query_analysis.depth" => 13,
+ "query_analysis.used_deprecated_fields" => an_instance_of(Array),
+ "query_analysis.used_fields" => an_instance_of(Array)
+ )
+ )
post_graphql(query, current_user: nil)
end
diff --git a/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb b/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb
index 30e704adb92..3527c8183f6 100644
--- a/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb
+++ b/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb
@@ -116,4 +116,26 @@ RSpec.describe 'getting dependency proxy manifests in a group' do
expect(dependency_proxy_image_count_response).to eq(manifests.size)
end
+
+ describe 'sorting and pagination' do
+ let(:data_path) { ['group', :dependencyProxyManifests] }
+ let(:current_user) { owner }
+
+ context 'with default sorting' do
+ let_it_be(:descending_manifests) { manifests.reverse.map { |manifest| global_id_of(manifest)} }
+
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { '' }
+ let(:first_param) { 2 }
+ let(:all_records) { descending_manifests }
+ end
+ end
+
+ def pagination_query(params)
+ # remove sort since the type does not accept sorting, but be future proof
+ graphql_query_for('group', { 'fullPath' => group.full_path },
+ query_nodes(:dependencyProxyManifests, :id, include_pagination_info: true, args: params.merge(sort: nil))
+ )
+ end
+ end
end
diff --git a/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb b/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb
index 0fd8fdc3f59..322706be119 100644
--- a/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe 'RunnersRegistrationTokenReset' do
subject
expect(graphql_errors).not_to be_empty
- expect(graphql_errors).to include(a_hash_including('message' => "The resource that you are attempting to access does not exist or you don't have permission to perform this action"))
+ expect(graphql_errors).to include(a_hash_including('message' => Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR))
expect(mutation_response).to be_nil
end
end
diff --git a/spec/requests/api/graphql/mutations/design_management/delete_spec.rb b/spec/requests/api/graphql/mutations/design_management/delete_spec.rb
index e329416faee..1dffb86b344 100644
--- a/spec/requests/api/graphql/mutations/design_management/delete_spec.rb
+++ b/spec/requests/api/graphql/mutations/design_management/delete_spec.rb
@@ -53,7 +53,7 @@ RSpec.describe "deleting designs" do
context 'the designs list contains filenames we cannot find' do
it_behaves_like 'a failed request' do
- let(:designs) { %w/foo bar baz/.map { |fn| OpenStruct.new(filename: fn) } }
+ let(:designs) { %w/foo bar baz/.map { |fn| instance_double('file', filename: fn) } }
let(:the_error) { a_string_matching %r/filenames were not found/ }
end
end
diff --git a/spec/requests/api/graphql/mutations/issues/create_spec.rb b/spec/requests/api/graphql/mutations/issues/create_spec.rb
index 886f3140086..6baed352b37 100644
--- a/spec/requests/api/graphql/mutations/issues/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/create_spec.rb
@@ -48,5 +48,9 @@ RSpec.describe 'Create an issue' do
expect(mutation_response['issue']).to include('discussionLocked' => true)
expect(Issue.last.work_item_type.base_type).to eq('issue')
end
+
+ it_behaves_like 'has spam protection' do
+ let(:mutation_class) { ::Mutations::Issues::Create }
+ end
end
end
diff --git a/spec/requests/api/graphql/mutations/issues/move_spec.rb b/spec/requests/api/graphql/mutations/issues/move_spec.rb
index 5bbaff61edd..20ed16879f6 100644
--- a/spec/requests/api/graphql/mutations/issues/move_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/move_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe 'Moving an issue' do
context 'when the user is not allowed to read source project' do
it 'returns an error' do
- error = "The resource that you are attempting to access does not exist or you don't have permission to perform this action"
+ error = Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR
post_graphql_mutation(mutation, current_user: user)
expect(response).to have_gitlab_http_status(:success)
diff --git a/spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb b/spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb
index 3f804a46992..12ab504da14 100644
--- a/spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe 'Setting an issue as confidential' do
end
it 'returns an error if the user is not allowed to update the issue' do
- error = "The resource that you are attempting to access does not exist or you don't have permission to perform this action"
+ error = Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR
post_graphql_mutation(mutation, current_user: create(:user))
expect(graphql_errors).to include(a_hash_including('message' => error))
diff --git a/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb b/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb
new file mode 100644
index 00000000000..3da702c55d7
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb
@@ -0,0 +1,161 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Setting issues crm contacts' do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:contacts) { create_list(:contact, 4, group: group) }
+
+ let(:issue) { create(:issue, project: project) }
+ let(:operation_mode) { Types::MutationOperationModeEnum.default_mode }
+ let(:crm_contact_ids) { [global_id_of(contacts[1]), global_id_of(contacts[2])] }
+ let(:does_not_exist_or_no_permission) { "The resource that you are attempting to access does not exist or you don't have permission to perform this action" }
+
+ let(:mutation) do
+ variables = {
+ project_path: issue.project.full_path,
+ iid: issue.iid.to_s,
+ operation_mode: operation_mode,
+ crm_contact_ids: crm_contact_ids
+ }
+
+ graphql_mutation(:issue_set_crm_contacts, variables,
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ issue {
+ customerRelationsContacts {
+ nodes {
+ id
+ }
+ }
+ }
+ QL
+ )
+ end
+
+ def mutation_response
+ graphql_mutation_response(:issue_set_crm_contacts)
+ end
+
+ before do
+ create(:issue_customer_relations_contact, issue: issue, contact: contacts[0])
+ create(:issue_customer_relations_contact, issue: issue, contact: contacts[1])
+ end
+
+ context 'when the user has no permission' do
+ it 'returns expected error' do
+ error = Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(graphql_errors).to include(a_hash_including('message' => error))
+ end
+ end
+
+ context 'when the user has permission' do
+ before do
+ group.add_reporter(user)
+ end
+
+ context 'when the feature is disabled' do
+ before do
+ stub_feature_flags(customer_relations: false)
+ end
+
+ it 'raises expected error' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(graphql_errors).to include(a_hash_including('message' => 'Feature disabled'))
+ end
+ end
+
+ context 'replace' do
+ it 'updates the issue with correct contacts' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes, :id))
+ .to match_array([global_id_of(contacts[1]), global_id_of(contacts[2])])
+ end
+ end
+
+ context 'append' do
+ let(:crm_contact_ids) { [global_id_of(contacts[3])] }
+ let(:operation_mode) { Types::MutationOperationModeEnum.enum[:append] }
+
+ it 'updates the issue with correct contacts' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes, :id))
+ .to match_array([global_id_of(contacts[0]), global_id_of(contacts[1]), global_id_of(contacts[3])])
+ end
+ end
+
+ context 'remove' do
+ let(:crm_contact_ids) { [global_id_of(contacts[0])] }
+ let(:operation_mode) { Types::MutationOperationModeEnum.enum[:remove] }
+
+ it 'updates the issue with correct contacts' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes, :id))
+ .to match_array([global_id_of(contacts[1])])
+ end
+ end
+
+ context 'when the contact does not exist' do
+ let(:crm_contact_ids) { ["gid://gitlab/CustomerRelations::Contact/#{non_existing_record_id}"] }
+
+ it 'returns expected error' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(graphql_data_at(:issue_set_crm_contacts, :errors))
+ .to match_array(["Issue customer relations contacts #{non_existing_record_id}: #{does_not_exist_or_no_permission}"])
+ end
+ end
+
+ context 'when the contact belongs to a different group' do
+ let(:group2) { create(:group) }
+ let(:contact) { create(:contact, group: group2) }
+ let(:crm_contact_ids) { [global_id_of(contact)] }
+
+ before do
+ group2.add_reporter(user)
+ end
+
+ it 'returns expected error' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(graphql_data_at(:issue_set_crm_contacts, :errors))
+ .to match_array(["Issue customer relations contacts #{contact.id}: #{does_not_exist_or_no_permission}"])
+ end
+ end
+
+ context 'when attempting to add more than 6' do
+ let(:operation_mode) { Types::MutationOperationModeEnum.enum[:append] }
+ let(:gid) { global_id_of(contacts[0]) }
+ let(:crm_contact_ids) { [gid, gid, gid, gid, gid, gid, gid] }
+
+ it 'returns expected error' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(graphql_data_at(:issue_set_crm_contacts, :errors))
+ .to match_array(["You can only add up to 6 contacts at one time"])
+ end
+ end
+
+ context 'when trying to remove non-existent contact' do
+ let(:operation_mode) { Types::MutationOperationModeEnum.enum[:remove] }
+ let(:crm_contact_ids) { ["gid://gitlab/CustomerRelations::Contact/#{non_existing_record_id}"] }
+
+ it 'raises expected error' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(graphql_data_at(:issue_set_crm_contacts, :errors)).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb b/spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb
index 72e47a98373..8e223b6fdaf 100644
--- a/spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe 'Setting Due Date of an issue' do
end
it 'returns an error if the user is not allowed to update the issue' do
- error = "The resource that you are attempting to access does not exist or you don't have permission to perform this action"
+ error = Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR
post_graphql_mutation(mutation, current_user: create(:user))
expect(graphql_errors).to include(a_hash_including('message' => error))
diff --git a/spec/requests/api/graphql/mutations/issues/set_severity_spec.rb b/spec/requests/api/graphql/mutations/issues/set_severity_spec.rb
index 41997f151a2..cd9d695bd2c 100644
--- a/spec/requests/api/graphql/mutations/issues/set_severity_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/set_severity_spec.rb
@@ -35,7 +35,7 @@ RSpec.describe 'Setting severity level of an incident' do
context 'when the user is not allowed to update the incident' do
it 'returns an error' do
- error = "The resource that you are attempting to access does not exist or you don't have permission to perform this action"
+ error = Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR
post_graphql_mutation(mutation, current_user: user)
expect(response).to have_gitlab_http_status(:success)
diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_wip_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_draft_spec.rb
index 2143abd3031..bea2365eaa6 100644
--- a/spec/requests/api/graphql/mutations/merge_requests/set_wip_spec.rb
+++ b/spec/requests/api/graphql/mutations/merge_requests/set_draft_spec.rb
@@ -8,14 +8,14 @@ RSpec.describe 'Setting Draft status of a merge request' do
let(:current_user) { create(:user) }
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
- let(:input) { { wip: true } }
+ let(:input) { { draft: true } }
let(:mutation) do
variables = {
project_path: project.full_path,
iid: merge_request.iid.to_s
}
- graphql_mutation(:merge_request_set_wip, variables.merge(input),
+ graphql_mutation(:merge_request_set_draft, variables.merge(input),
<<-QL.strip_heredoc
clientMutationId
errors
@@ -28,7 +28,7 @@ RSpec.describe 'Setting Draft status of a merge request' do
end
def mutation_response
- graphql_mutation_response(:merge_request_set_wip)
+ graphql_mutation_response(:merge_request_set_draft)
end
before do
@@ -58,7 +58,7 @@ RSpec.describe 'Setting Draft status of a merge request' do
end
context 'when passing Draft false as input' do
- let(:input) { { wip: false } }
+ let(:input) { { draft: false } }
it 'does not do anything if the merge reqeust was not marked draft' do
post_graphql_mutation(mutation, current_user: current_user)
diff --git a/spec/requests/api/graphql/mutations/merge_requests/update_reviewer_state_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/update_reviewer_state_spec.rb
new file mode 100644
index 00000000000..cf497cb2579
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/merge_requests/update_reviewer_state_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Toggle attention requested for reviewer' 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_toggle_attention_requested, variables.merge(input),
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ QL
+ )
+ end
+
+ def mutation_response
+ graphql_mutation_response(:merge_request_toggle_attention_requested)
+ 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/releases/create_spec.rb b/spec/requests/api/graphql/mutations/releases/create_spec.rb
index a4918cd560c..86995c10f10 100644
--- a/spec/requests/api/graphql/mutations/releases/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/releases/create_spec.rb
@@ -342,7 +342,7 @@ RSpec.describe 'Creation of a new release' do
end
context "when the current user doesn't have access to create releases" do
- expected_error_message = "The resource that you are attempting to access does not exist or you don't have permission to perform this action"
+ expected_error_message = Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR
context 'when the current user is a Reporter' do
let(:current_user) { reporter }
diff --git a/spec/requests/api/graphql/mutations/releases/delete_spec.rb b/spec/requests/api/graphql/mutations/releases/delete_spec.rb
index 40063156609..eb4f0b594ea 100644
--- a/spec/requests/api/graphql/mutations/releases/delete_spec.rb
+++ b/spec/requests/api/graphql/mutations/releases/delete_spec.rb
@@ -50,7 +50,7 @@ RSpec.describe 'Deleting a release' do
expect(mutation_response).to be_nil
expect(graphql_errors.count).to eq(1)
- expect(graphql_errors.first['message']).to eq("The resource that you are attempting to access does not exist or you don't have permission to perform this action")
+ expect(graphql_errors.first['message']).to eq(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
end
end
diff --git a/spec/requests/api/graphql/mutations/releases/update_spec.rb b/spec/requests/api/graphql/mutations/releases/update_spec.rb
index c9a6c3abd57..0fa3d7de299 100644
--- a/spec/requests/api/graphql/mutations/releases/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/releases/update_spec.rb
@@ -218,13 +218,13 @@ RSpec.describe 'Updating an existing release' do
context 'when the project does not exist' do
let(:mutation_arguments) { super().merge(projectPath: 'not/a/real/path') }
- it_behaves_like 'top-level error with message', "The resource that you are attempting to access does not exist or you don't have permission to perform this action"
+ it_behaves_like 'top-level error with message', Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR
end
end
end
context "when the current user doesn't have access to update releases" do
- expected_error_message = "The resource that you are attempting to access does not exist or you don't have permission to perform this action"
+ expected_error_message = Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR
context 'when the current user is a Reporter' do
let(:current_user) { reporter }
diff --git a/spec/requests/api/graphql/mutations/security/ci_configuration/configure_sast_iac_spec.rb b/spec/requests/api/graphql/mutations/security/ci_configuration/configure_sast_iac_spec.rb
new file mode 100644
index 00000000000..929609d4160
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/security/ci_configuration/configure_sast_iac_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'ConfigureSastIac' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :test_repo) }
+
+ let(:variables) { { project_path: project.full_path } }
+ let(:mutation) { graphql_mutation(:configure_sast_iac, variables) }
+ let(:mutation_response) { graphql_mutation_response(:configureSastIac) }
+
+ context 'when authorized' do
+ let_it_be(:user) { project.owner }
+
+ it 'creates a branch with sast iac configured' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['errors']).to be_empty
+ expect(mutation_response['branch']).not_to be_empty
+ expect(mutation_response['successPath']).not_to be_empty
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/namespace_query_spec.rb b/spec/requests/api/graphql/namespace_query_spec.rb
new file mode 100644
index 00000000000..f7ee2bcb55d
--- /dev/null
+++ b/spec/requests/api/graphql/namespace_query_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query' do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:other_user) { create(:user) }
+
+ let_it_be(:group_namespace) { create(:group) }
+ let_it_be(:user_namespace) { create(:user_namespace, owner: user) }
+ let_it_be(:project_namespace) { create(:project_namespace, parent: group_namespace) }
+
+ describe '.namespace' do
+ subject { post_graphql(query, current_user: current_user) }
+
+ let(:current_user) { user }
+
+ let(:query) { graphql_query_for(:namespace, { 'fullPath' => target_namespace.full_path }, all_graphql_fields_for('Namespace')) }
+ let(:query_result) { graphql_data['namespace'] }
+
+ shared_examples 'retrieving a namespace' do
+ context 'authorised query' do
+ before do
+ subject
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'fetches the expected data' do
+ expect(query_result).to include(
+ 'fullPath' => target_namespace.full_path,
+ 'name' => target_namespace.name
+ )
+ end
+ end
+
+ context 'unauthorised query' do
+ before do
+ subject
+ end
+
+ context 'anonymous user' do
+ let(:current_user) { nil }
+
+ it 'does not retrieve the record' do
+ expect(query_result).to be_nil
+ end
+ end
+
+ context 'the current user does not have permission' do
+ let(:current_user) { other_user }
+
+ it 'does not retrieve the record' do
+ expect(query_result).to be_nil
+ end
+ end
+ end
+ end
+
+ it_behaves_like 'retrieving a namespace' do
+ let(:target_namespace) { group_namespace }
+
+ before do
+ group_namespace.add_developer(user)
+ end
+ end
+
+ it_behaves_like 'retrieving a namespace' do
+ let(:target_namespace) { user_namespace }
+ end
+
+ context 'does not retrieve project namespace' do
+ let(:target_namespace) { project_namespace }
+
+ before do
+ subject
+ end
+
+ it 'does not retrieve the record' do
+ expect(query_result).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/packages/helm_spec.rb b/spec/requests/api/graphql/packages/helm_spec.rb
new file mode 100644
index 00000000000..397096f70db
--- /dev/null
+++ b/spec/requests/api/graphql/packages/helm_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe 'helm package details' do
+ include GraphqlHelpers
+ include_context 'package details setup'
+
+ let_it_be(:package) { create(:helm_package, project: project) }
+
+ let(:package_files_metadata) {query_graphql_fragment('HelmFileMetadata')}
+
+ let(:query) do
+ graphql_query_for(:package, { id: package_global_id }, <<~FIELDS)
+ #{all_graphql_fields_for('PackageDetailsType', max_depth: depth, excluded: excluded)}
+ packageFiles {
+ nodes {
+ #{package_files}
+ fileMetadata {
+ #{package_files_metadata}
+ }
+ }
+ }
+ FIELDS
+ end
+
+ subject { post_graphql(query, current_user: user) }
+
+ before do
+ subject
+ end
+
+ it_behaves_like 'a package detail'
+ it_behaves_like 'a package with files'
+
+ it 'has the correct file metadata' do
+ expect(first_file_response_metadata).to include(
+ 'channel' => first_file.helm_file_metadatum.channel
+ )
+ expect(first_file_response_metadata['metadata']).to include(
+ 'name' => first_file.helm_file_metadatum.metadata['name'],
+ 'home' => first_file.helm_file_metadatum.metadata['home'],
+ 'sources' => first_file.helm_file_metadatum.metadata['sources'],
+ 'version' => first_file.helm_file_metadatum.metadata['version'],
+ 'description' => first_file.helm_file_metadatum.metadata['description'],
+ 'keywords' => first_file.helm_file_metadatum.metadata['keywords'],
+ 'maintainers' => first_file.helm_file_metadatum.metadata['maintainers'],
+ 'icon' => first_file.helm_file_metadatum.metadata['icon'],
+ 'apiVersion' => first_file.helm_file_metadatum.metadata['apiVersion'],
+ 'condition' => first_file.helm_file_metadatum.metadata['condition'],
+ 'tags' => first_file.helm_file_metadatum.metadata['tags'],
+ 'appVersion' => first_file.helm_file_metadatum.metadata['appVersion'],
+ 'deprecated' => first_file.helm_file_metadatum.metadata['deprecated'],
+ 'annotations' => first_file.helm_file_metadatum.metadata['annotations'],
+ 'kubeVersion' => first_file.helm_file_metadatum.metadata['kubeVersion'],
+ 'dependencies' => first_file.helm_file_metadatum.metadata['dependencies'],
+ 'type' => first_file.helm_file_metadatum.metadata['type']
+ )
+ end
+end
diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb
index 1c6d6ce4707..b3e91afb5b3 100644
--- a/spec/requests/api/graphql/project/issues_spec.rb
+++ b/spec/requests/api/graphql/project/issues_spec.rb
@@ -429,11 +429,11 @@ RSpec.describe 'getting an issue list for a project' do
end
it 'avoids N+1 queries' do
- create(:contact, group_id: group.id, issues: [issue_a])
+ create(:issue_customer_relations_contact, :for_issue, issue: issue_a)
control = ActiveRecord::QueryRecorder.new(skip_cached: false) { clean_state_query }
- create(:contact, group_id: group.id, issues: [issue_a])
+ create(:issue_customer_relations_contact, :for_issue, issue: issue_a)
expect { clean_state_query }.not_to exceed_all_query_limit(control)
end
diff --git a/spec/requests/api/graphql/project/merge_request_spec.rb b/spec/requests/api/graphql/project/merge_request_spec.rb
index 438ea9bb4c1..353bf0356f6 100644
--- a/spec/requests/api/graphql/project/merge_request_spec.rb
+++ b/spec/requests/api/graphql/project/merge_request_spec.rb
@@ -347,7 +347,7 @@ RSpec.describe 'getting merge request information nested in a project' do
expect(interaction_data).to contain_exactly a_hash_including(
'canMerge' => false,
'canUpdate' => can_update,
- 'reviewState' => unreviewed,
+ 'reviewState' => attention_requested,
'reviewed' => false,
'approved' => false
)
@@ -380,8 +380,8 @@ RSpec.describe 'getting merge request information nested in a project' do
describe 'scalability' do
let_it_be(:other_users) { create_list(:user, 3) }
- let(:unreviewed) do
- { 'reviewState' => 'UNREVIEWED' }
+ let(:attention_requested) do
+ { 'reviewState' => 'ATTENTION_REQUESTED' }
end
let(:reviewed) do
@@ -413,9 +413,9 @@ RSpec.describe 'getting merge request information nested in a project' do
expect { post_graphql(query) }.not_to exceed_query_limit(baseline)
expect(interaction_data).to contain_exactly(
- include(unreviewed),
- include(unreviewed),
- include(unreviewed),
+ include(attention_requested),
+ include(attention_requested),
+ include(attention_requested),
include(reviewed)
)
end
@@ -444,7 +444,7 @@ RSpec.describe 'getting merge request information nested in a project' do
it_behaves_like 'when requesting information about MR interactions' do
let(:field) { :reviewers }
- let(:unreviewed) { 'UNREVIEWED' }
+ let(:attention_requested) { 'ATTENTION_REQUESTED' }
let(:can_update) { false }
def assign_user(user)
@@ -454,7 +454,7 @@ RSpec.describe 'getting merge request information nested in a project' do
it_behaves_like 'when requesting information about MR interactions' do
let(:field) { :assignees }
- let(:unreviewed) { nil }
+ let(:attention_requested) { nil }
let(:can_update) { true } # assignees can update MRs
def assign_user(user)
diff --git a/spec/requests/api/graphql/project/release_spec.rb b/spec/requests/api/graphql/project/release_spec.rb
index 7f24d051457..77abac4ef04 100644
--- a/spec/requests/api/graphql/project/release_spec.rb
+++ b/spec/requests/api/graphql/project/release_spec.rb
@@ -228,6 +228,189 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
end
end
+ shared_examples 'restricted access to release fields' do
+ describe 'scalar fields' do
+ let(:path) { path_prefix }
+
+ let(:release_fields) do
+ %{
+ tagName
+ tagPath
+ description
+ descriptionHtml
+ name
+ createdAt
+ releasedAt
+ upcomingRelease
+ }
+ end
+
+ before do
+ post_query
+ end
+
+ it 'finds all release data' do
+ expect(data).to eq({
+ 'tagName' => release.tag,
+ 'tagPath' => nil,
+ 'description' => release.description,
+ 'descriptionHtml' => release.description_html,
+ 'name' => release.name,
+ 'createdAt' => release.created_at.iso8601,
+ 'releasedAt' => release.released_at.iso8601,
+ 'upcomingRelease' => false
+ })
+ end
+ end
+
+ describe 'milestones' do
+ let(:path) { path_prefix + %w[milestones nodes] }
+
+ let(:release_fields) do
+ query_graphql_field(:milestones, nil, 'nodes { id title }')
+ end
+
+ it 'finds milestones associated to a release' do
+ post_query
+
+ expected = release.milestones.order_by_dates_and_title.map do |milestone|
+ { 'id' => global_id_of(milestone), 'title' => milestone.title }
+ end
+
+ expect(data).to eq(expected)
+ end
+ end
+
+ describe 'author' do
+ let(:path) { path_prefix + %w[author] }
+
+ let(:release_fields) do
+ query_graphql_field(:author, nil, 'id username')
+ end
+
+ it 'finds the author of the release' do
+ post_query
+
+ expect(data).to eq(
+ 'id' => global_id_of(release.author),
+ 'username' => release.author.username
+ )
+ end
+ end
+
+ describe 'commit' do
+ let(:path) { path_prefix + %w[commit] }
+
+ let(:release_fields) do
+ query_graphql_field(:commit, nil, 'sha')
+ end
+
+ it 'restricts commit associated with the release' do
+ post_query
+
+ expect(data).to eq(nil)
+ end
+ end
+
+ describe 'assets' do
+ describe 'count' do
+ let(:path) { path_prefix + %w[assets] }
+
+ let(:release_fields) do
+ query_graphql_field(:assets, nil, 'count')
+ end
+
+ it 'returns non source release links count' do
+ post_query
+
+ expect(data).to eq('count' => release.assets_count(except: [:sources]))
+ end
+ end
+
+ describe 'links' do
+ let(:path) { path_prefix + %w[assets links nodes] }
+
+ let(:release_fields) do
+ query_graphql_field(:assets, nil,
+ query_graphql_field(:links, nil, 'nodes { id name url external, directAssetUrl }'))
+ end
+
+ it 'finds all non source external release links' do
+ post_query
+
+ expected = release.links.map do |link|
+ {
+ 'id' => global_id_of(link),
+ 'name' => link.name,
+ 'url' => link.url,
+ 'external' => true,
+ 'directAssetUrl' => link.filepath ? Gitlab::Routing.url_helpers.project_release_url(project, release) << "/downloads#{link.filepath}" : link.url
+ }
+ end
+
+ expect(data).to match_array(expected)
+ end
+ end
+
+ describe 'sources' do
+ let(:path) { path_prefix + %w[assets sources nodes] }
+
+ let(:release_fields) do
+ query_graphql_field(:assets, nil,
+ query_graphql_field(:sources, nil, 'nodes { format url }'))
+ end
+
+ it 'restricts release sources' do
+ post_query
+
+ expect(data).to match_array([])
+ end
+ end
+ end
+
+ describe 'links' do
+ let(:path) { path_prefix + %w[links] }
+
+ let(:release_fields) do
+ query_graphql_field(:links, nil, %{
+ selfUrl
+ openedMergeRequestsUrl
+ mergedMergeRequestsUrl
+ closedMergeRequestsUrl
+ openedIssuesUrl
+ closedIssuesUrl
+ })
+ end
+
+ it 'finds only selfUrl' do
+ post_query
+
+ expect(data).to eq(
+ 'selfUrl' => project_release_url(project, release),
+ 'openedMergeRequestsUrl' => nil,
+ 'mergedMergeRequestsUrl' => nil,
+ 'closedMergeRequestsUrl' => nil,
+ 'openedIssuesUrl' => nil,
+ 'closedIssuesUrl' => nil
+ )
+ end
+ end
+
+ describe 'evidences' do
+ let(:path) { path_prefix + %w[evidences] }
+
+ let(:release_fields) do
+ query_graphql_field(:evidences, nil, 'nodes { id sha filepath collectedAt }')
+ end
+
+ it 'restricts all evidence fields' do
+ post_query
+
+ expect(data).to eq('nodes' => [])
+ end
+ end
+ end
+
shared_examples 'no access to the release field' do
describe 'repository-related fields' do
let(:path) { path_prefix }
@@ -302,7 +485,8 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
context 'when the user has Guest permissions' do
let(:current_user) { guest }
- it_behaves_like 'no access to the release field'
+ it_behaves_like 'restricted access to release fields'
+ it_behaves_like 'no access to editUrl'
end
context 'when the user has Reporter permissions' do
diff --git a/spec/requests/api/graphql/project/releases_spec.rb b/spec/requests/api/graphql/project/releases_spec.rb
index 2816ce90a6b..c28a6fa7666 100644
--- a/spec/requests/api/graphql/project/releases_spec.rb
+++ b/spec/requests/api/graphql/project/releases_spec.rb
@@ -129,10 +129,12 @@ RSpec.describe 'Query.project(fullPath).releases()' do
end
it 'does not return data for fields that expose repository information' do
+ tag_name = release.tag
+ release_name = release.name
expect(data).to eq(
- 'tagName' => nil,
+ 'tagName' => tag_name,
'tagPath' => nil,
- 'name' => "Release-#{release.id}",
+ 'name' => release_name,
'commit' => nil,
'assets' => {
'count' => release.assets_count(except: [:sources]),
@@ -143,7 +145,14 @@ RSpec.describe 'Query.project(fullPath).releases()' do
'evidences' => {
'nodes' => []
},
- 'links' => nil
+ 'links' => {
+ 'closedIssuesUrl' => nil,
+ 'closedMergeRequestsUrl' => nil,
+ 'mergedMergeRequestsUrl' => nil,
+ 'openedIssuesUrl' => nil,
+ 'openedMergeRequestsUrl' => nil,
+ 'selfUrl' => project_release_url(project, release)
+ }
)
end
end
diff --git a/spec/requests/api/graphql_spec.rb b/spec/requests/api/graphql_spec.rb
index 7d182a3414b..b8f7af29a9f 100644
--- a/spec/requests/api/graphql_spec.rb
+++ b/spec/requests/api/graphql_spec.rb
@@ -12,21 +12,33 @@ RSpec.describe 'GraphQL' do
describe 'logging' do
shared_examples 'logging a graphql query' do
- let(:expected_params) do
+ let(:expected_execute_query_log) do
{
- query_string: query,
- variables: variables.to_s,
- duration_s: anything,
+ "correlation_id" => kind_of(String),
+ "meta.caller_id" => "graphql:anonymous",
+ "meta.client_id" => kind_of(String),
+ "meta.feature_category" => "not_owned",
+ "meta.remote_ip" => kind_of(String),
+ "query_analysis.duration_s" => kind_of(Numeric),
+ "query_analysis.depth" => 1,
+ "query_analysis.complexity" => 1,
+ "query_analysis.used_fields" => ['Query.echo'],
+ "query_analysis.used_deprecated_fields" => [],
+ # query_fingerprint starts with operation name
+ query_fingerprint: %r{^anonymous\/},
+ duration_s: kind_of(Numeric),
+ trace_type: 'execute_query',
operation_name: nil,
- depth: 1,
- complexity: 1,
- used_fields: ['Query.echo'],
- used_deprecated_fields: []
+ # operation_fingerprint starts with operation name
+ operation_fingerprint: %r{^anonymous\/},
+ is_mutation: false,
+ variables: variables.to_s,
+ query_string: query
}
end
it 'logs a query with the expected params' do
- expect(Gitlab::GraphqlLogger).to receive(:info).with(expected_params).once
+ expect(Gitlab::GraphqlLogger).to receive(:info).with(expected_execute_query_log).once
post_graphql(query, variables: variables)
end
diff --git a/spec/requests/api/group_debian_distributions_spec.rb b/spec/requests/api/group_debian_distributions_spec.rb
index ec1912b72bf..21c5f2f09a0 100644
--- a/spec/requests/api/group_debian_distributions_spec.rb
+++ b/spec/requests/api/group_debian_distributions_spec.rb
@@ -11,19 +11,25 @@ RSpec.describe API::GroupDebianDistributions do
let(:url) { "/groups/#{container.id}/-/debian_distributions" }
let(:api_params) { { 'codename': 'my-codename' } }
- it_behaves_like 'Debian repository write endpoint', 'POST distribution request', :created, /^{.*"codename":"my-codename",.*"components":\["main"\],.*"architectures":\["all","amd64"\]/, authenticate_non_public: false
+ it_behaves_like 'Debian distributions write endpoint', 'POST', :created, /^{.*"codename":"my-codename",.*"components":\["main"\],.*"architectures":\["all","amd64"\]/
end
describe 'GET groups/:id/-/debian_distributions' do
let(:url) { "/groups/#{container.id}/-/debian_distributions" }
- it_behaves_like 'Debian repository read endpoint', 'GET request', :success, /^\[{.*"codename":"existing-codename",.*"components":\["existing-component"\],.*"architectures":\["all","existing-arch"\]/, authenticate_non_public: false
+ it_behaves_like 'Debian distributions read endpoint', 'GET', :success, /^\[{.*"codename":"existing-codename",.*"components":\["existing-component"\],.*"architectures":\["all","existing-arch"\]/
end
describe 'GET groups/:id/-/debian_distributions/:codename' do
let(:url) { "/groups/#{container.id}/-/debian_distributions/#{distribution.codename}" }
- it_behaves_like 'Debian repository read endpoint', 'GET request', :success, /^{.*"codename":"existing-codename",.*"components":\["existing-component"\],.*"architectures":\["all","existing-arch"\]/, authenticate_non_public: false
+ it_behaves_like 'Debian distributions read endpoint', 'GET', :success, /^{.*"codename":"existing-codename",.*"components":\["existing-component"\],.*"architectures":\["all","existing-arch"\]/
+ end
+
+ describe 'GET groups/:id/-/debian_distributions/:codename/key.asc' do
+ let(:url) { "/groups/#{container.id}/-/debian_distributions/#{distribution.codename}/key.asc" }
+
+ it_behaves_like 'Debian distributions read endpoint', 'GET', :success, /^-----BEGIN PGP PUBLIC KEY BLOCK-----/
end
describe 'PUT groups/:id/-/debian_distributions/:codename' do
@@ -31,14 +37,14 @@ RSpec.describe API::GroupDebianDistributions do
let(:url) { "/groups/#{container.id}/-/debian_distributions/#{distribution.codename}" }
let(:api_params) { { suite: 'my-suite' } }
- it_behaves_like 'Debian repository write endpoint', 'PUT distribution request', :success, /^{.*"codename":"existing-codename",.*"suite":"my-suite",/, authenticate_non_public: false
+ it_behaves_like 'Debian distributions write endpoint', 'PUT', :success, /^{.*"codename":"existing-codename",.*"suite":"my-suite",/
end
describe 'DELETE groups/:id/-/debian_distributions/:codename' do
let(:method) { :delete }
let(:url) { "/groups/#{container.id}/-/debian_distributions/#{distribution.codename}" }
- it_behaves_like 'Debian repository maintainer write endpoint', 'DELETE distribution request', :success, /^{"message":"202 Accepted"}$/, authenticate_non_public: false
+ it_behaves_like 'Debian distributions maintainer write endpoint', 'DELETE', :success, /^{"message":"202 Accepted"}$/
end
end
end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index cee727ae6fe..75f5a974d22 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -319,12 +319,15 @@ RSpec.describe API::Groups do
it "includes statistics if requested" do
attributes = {
- storage_size: 2392,
+ storage_size: 4093,
repository_size: 123,
wiki_size: 456,
lfs_objects_size: 234,
build_artifacts_size: 345,
- snippets_size: 1234
+ pipeline_artifacts_size: 456,
+ packages_size: 567,
+ snippets_size: 1234,
+ uploads_size: 678
}.stringify_keys
exposed_attributes = attributes.dup
exposed_attributes['job_artifacts_size'] = exposed_attributes.delete('build_artifacts_size')
diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb
index aeca4e435f4..0a71eb43f81 100644
--- a/spec/requests/api/internal/base_spec.rb
+++ b/spec/requests/api/internal/base_spec.rb
@@ -948,7 +948,7 @@ RSpec.describe API::Internal::Base do
context 'user does not exist' do
it do
- pull(OpenStruct.new(id: 0), project)
+ pull(double('key', id: 0), project)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response["status"]).to be_falsey
diff --git a/spec/requests/api/invitations_spec.rb b/spec/requests/api/invitations_spec.rb
index b23ba0021e0..cba4256adc5 100644
--- a/spec/requests/api/invitations_spec.rb
+++ b/spec/requests/api/invitations_spec.rb
@@ -166,6 +166,38 @@ RSpec.describe API::Invitations do
end
end
+ context 'with tasks_to_be_done and tasks_project_id in the params' do
+ before do
+ stub_experiments(invite_members_for_task: true)
+ end
+
+ let(:project_id) { source_type == 'project' ? source.id : create(:project, namespace: source).id }
+
+ context 'when there is 1 invitation' do
+ it 'creates a member_task with the tasks_to_be_done and the project' do
+ post invitations_url(source, maintainer),
+ params: { email: email, access_level: Member::DEVELOPER, tasks_to_be_done: %w(code ci), tasks_project_id: project_id }
+
+ member = source.members.find_by(invite_email: email)
+ expect(member.tasks_to_be_done).to match_array([:code, :ci])
+ expect(member.member_task.project_id).to eq(project_id)
+ end
+ end
+
+ context 'when there are multiple invitations' do
+ it 'creates a member_task with the tasks_to_be_done and the project' do
+ post invitations_url(source, maintainer),
+ params: { email: [email, email2].join(','), access_level: Member::DEVELOPER, tasks_to_be_done: %w(code ci), tasks_project_id: project_id }
+
+ members = source.members.where(invite_email: [email, email2])
+ members.each do |member|
+ expect(member.tasks_to_be_done).to match_array([:code, :ci])
+ expect(member.member_task.project_id).to eq(project_id)
+ end
+ end
+ end
+ end
+
context 'with invite_source considerations', :snowplow do
let(:params) { { email: email, access_level: Member::DEVELOPER } }
diff --git a/spec/requests/api/lint_spec.rb b/spec/requests/api/lint_spec.rb
index d7f22b9d619..ac30da99afe 100644
--- a/spec/requests/api/lint_spec.rb
+++ b/spec/requests/api/lint_spec.rb
@@ -102,6 +102,13 @@ RSpec.describe API::Lint do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to have_key('merged_yaml')
end
+
+ it 'outputs jobs' do
+ post api('/ci/lint', api_user), params: { content: yaml_content, include_jobs: true }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to have_key('jobs')
+ end
end
context 'with valid .gitlab-ci.yaml with warnings' do
@@ -136,6 +143,13 @@ RSpec.describe API::Lint do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to have_key('merged_yaml')
end
+
+ it 'outputs jobs' do
+ post api('/ci/lint', api_user), params: { content: yaml_content, include_jobs: true }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to have_key('jobs')
+ end
end
context 'with invalid configuration' do
@@ -156,6 +170,13 @@ RSpec.describe API::Lint do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to have_key('merged_yaml')
end
+
+ it 'outputs jobs' do
+ post api('/ci/lint', api_user), params: { content: yaml_content, include_jobs: true }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to have_key('jobs')
+ end
end
end
@@ -171,10 +192,11 @@ RSpec.describe API::Lint do
end
describe 'GET /projects/:id/ci/lint' do
- subject(:ci_lint) { get api("/projects/#{project.id}/ci/lint", api_user), params: { dry_run: dry_run } }
+ subject(:ci_lint) { get api("/projects/#{project.id}/ci/lint", api_user), params: { dry_run: dry_run, include_jobs: include_jobs } }
let(:project) { create(:project, :repository) }
let(:dry_run) { nil }
+ let(:include_jobs) { nil }
RSpec.shared_examples 'valid config with warnings' do
it 'passes validation with warnings' do
@@ -359,6 +381,30 @@ RSpec.describe API::Lint do
it_behaves_like 'valid config without warnings'
end
+ context 'when running with include jobs' do
+ let(:include_jobs) { true }
+
+ it_behaves_like 'valid config without warnings'
+
+ it 'returns jobs key' do
+ ci_lint
+
+ expect(json_response).to have_key('jobs')
+ end
+ end
+
+ context 'when running without include jobs' do
+ let(:include_jobs) { false }
+
+ it_behaves_like 'valid config without warnings'
+
+ it 'does not return jobs key' do
+ ci_lint
+
+ expect(json_response).not_to have_key('jobs')
+ end
+ end
+
context 'With warnings' do
let(:yaml_content) { { job: { script: 'ls', rules: [{ when: 'always' }] } }.to_yaml }
@@ -386,15 +432,40 @@ RSpec.describe API::Lint do
it_behaves_like 'invalid config'
end
+
+ context 'when running with include jobs' do
+ let(:include_jobs) { true }
+
+ it_behaves_like 'invalid config'
+
+ it 'returns jobs key' do
+ ci_lint
+
+ expect(json_response).to have_key('jobs')
+ end
+ end
+
+ context 'when running without include jobs' do
+ let(:include_jobs) { false }
+
+ it_behaves_like 'invalid config'
+
+ it 'does not return jobs key' do
+ ci_lint
+
+ expect(json_response).not_to have_key('jobs')
+ end
+ end
end
end
end
describe 'POST /projects/:id/ci/lint' do
- subject(:ci_lint) { post api("/projects/#{project.id}/ci/lint", api_user), params: { dry_run: dry_run, content: yaml_content } }
+ subject(:ci_lint) { post api("/projects/#{project.id}/ci/lint", api_user), params: { dry_run: dry_run, content: yaml_content, include_jobs: include_jobs } }
let(:project) { create(:project, :repository) }
let(:dry_run) { nil }
+ let(:include_jobs) { nil }
let_it_be(:api_user) { create(:user) }
@@ -562,6 +633,30 @@ RSpec.describe API::Lint do
it_behaves_like 'valid project config'
end
+
+ context 'when running with include jobs param' do
+ let(:include_jobs) { true }
+
+ it_behaves_like 'valid project config'
+
+ it 'contains jobs key' do
+ ci_lint
+
+ expect(json_response).to have_key('jobs')
+ end
+ end
+
+ context 'when running without include jobs param' do
+ let(:include_jobs) { false }
+
+ it_behaves_like 'valid project config'
+
+ it 'does not contain jobs key' do
+ ci_lint
+
+ expect(json_response).not_to have_key('jobs')
+ end
+ end
end
context 'with invalid .gitlab-ci.yml content' do
@@ -580,6 +675,30 @@ RSpec.describe API::Lint do
it_behaves_like 'invalid project config'
end
+
+ context 'when running with include jobs set to false' do
+ let(:include_jobs) { false }
+
+ it_behaves_like 'invalid project config'
+
+ it 'does not contain jobs key' do
+ ci_lint
+
+ expect(json_response).not_to have_key('jobs')
+ end
+ end
+
+ context 'when running with param include jobs' do
+ let(:include_jobs) { true }
+
+ it_behaves_like 'invalid project config'
+
+ it 'contains jobs key' do
+ ci_lint
+
+ expect(json_response).to have_key('jobs')
+ end
+ end
end
end
end
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index a1daf86de31..7f4345faabb 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -81,14 +81,22 @@ RSpec.describe API::Members do
expect(json_response.map { |u| u['id'] }).to match_array [maintainer.id, developer.id]
end
- it 'finds members with query string' do
- get api(members_url, developer), params: { query: maintainer.username }
+ context 'with cross db check disabled' do
+ around do |example|
+ allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/343305') do
+ example.run
+ end
+ end
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.count).to eq(1)
- expect(json_response.first['username']).to eq(maintainer.username)
+ it 'finds members with query string' do
+ get api(members_url, developer), params: { query: maintainer.username }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.count).to eq(1)
+ expect(json_response.first['username']).to eq(maintainer.username)
+ end
end
it 'finds members with the given user_ids' do
@@ -406,6 +414,38 @@ RSpec.describe API::Members do
end
end
+ context 'with tasks_to_be_done and tasks_project_id in the params' do
+ before do
+ stub_experiments(invite_members_for_task: true)
+ end
+
+ let(:project_id) { source_type == 'project' ? source.id : create(:project, namespace: source).id }
+
+ context 'when there is 1 user to add' do
+ it 'creates a member_task with the correct attributes' do
+ post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
+ params: { user_id: stranger.id, access_level: Member::DEVELOPER, tasks_to_be_done: %w(code ci), tasks_project_id: project_id }
+
+ member = source.members.find_by(user_id: stranger.id)
+ expect(member.tasks_to_be_done).to match_array([:code, :ci])
+ expect(member.member_task.project_id).to eq(project_id)
+ end
+ end
+
+ context 'when there are multiple users to add' do
+ it 'creates a member_task with the correct attributes' do
+ post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
+ params: { user_id: [developer.id, stranger.id].join(','), access_level: Member::DEVELOPER, tasks_to_be_done: %w(code ci), tasks_project_id: project_id }
+
+ members = source.members.where(user_id: [developer.id, stranger.id])
+ members.each do |member|
+ expect(member.tasks_to_be_done).to match_array([:code, :ci])
+ expect(member.member_task.project_id).to eq(project_id)
+ end
+ end
+ end
+ end
+
it "returns 409 if member already exists" do
post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
params: { user_id: maintainer.id, access_level: Member::MAINTAINER }
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index bdbc73a59d8..7c147419354 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -3278,6 +3278,8 @@ RSpec.describe API::MergeRequests do
context 'when skip_ci parameter is set' do
it 'enqueues a rebase of the merge request with skip_ci flag set' do
+ allow(RebaseWorker).to receive(:with_status).and_return(RebaseWorker)
+
expect(RebaseWorker).to receive(:perform_async).with(merge_request.id, user.id, true).and_call_original
Sidekiq::Testing.fake! do
diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb
index 222d8992d1b..01dbf523071 100644
--- a/spec/requests/api/namespaces_spec.rb
+++ b/spec/requests/api/namespaces_spec.rb
@@ -3,10 +3,12 @@
require 'spec_helper'
RSpec.describe API::Namespaces do
- let(:admin) { create(:admin) }
- let(:user) { create(:user) }
- let!(:group1) { create(:group, name: 'group.one') }
- let!(:group2) { create(:group, :nested) }
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group1) { create(:group, name: 'group.one') }
+ let_it_be(:group2) { create(:group, :nested) }
+ let_it_be(:project) { create(:project, namespace: group2, name: group2.name, path: group2.path) }
+ let_it_be(:project_namespace) { project.project_namespace }
describe "GET /namespaces" do
context "when unauthenticated" do
@@ -26,7 +28,7 @@ RSpec.describe API::Namespaces do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(group_kind_json_response.keys).to include('id', 'kind', 'name', 'path', 'full_path',
- 'parent_id', 'members_count_with_descendants')
+ 'parent_id', 'members_count_with_descendants')
expect(user_kind_json_response.keys).to include('id', 'kind', 'name', 'path', 'full_path', 'parent_id')
end
@@ -37,7 +39,8 @@ RSpec.describe API::Namespaces do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.length).to eq(Namespace.count)
+ # project namespace is excluded
+ expect(json_response.length).to eq(Namespace.count - 1)
end
it "admin: returns an array of matched namespaces" do
@@ -61,7 +64,7 @@ RSpec.describe API::Namespaces do
owned_group_response = json_response.find { |resource| resource['id'] == group1.id }
expect(owned_group_response.keys).to include('id', 'kind', 'name', 'path', 'full_path',
- 'parent_id', 'members_count_with_descendants')
+ 'parent_id', 'members_count_with_descendants')
end
it "returns correct attributes when user cannot admin group" do
@@ -109,7 +112,8 @@ RSpec.describe API::Namespaces do
describe 'GET /namespaces/:id' do
let(:owned_group) { group1 }
- let(:user2) { create(:user) }
+
+ let_it_be(:user2) { create(:user) }
shared_examples 'can access namespace' do
it 'returns namespace details' do
@@ -144,6 +148,16 @@ RSpec.describe API::Namespaces do
it_behaves_like 'can access namespace'
end
+
+ context 'when requesting project_namespace' do
+ let(:namespace_id) { project_namespace.id }
+
+ it 'returns not-found' do
+ get api("/namespaces/#{namespace_id}", request_actor)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
end
context 'when requested by path' do
@@ -159,6 +173,16 @@ RSpec.describe API::Namespaces do
it_behaves_like 'can access namespace'
end
+
+ context 'when requesting project_namespace' do
+ let(:namespace_id) { project_namespace.full_path }
+
+ it 'returns not-found' do
+ get api("/namespaces/#{namespace_id}", request_actor)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
end
end
@@ -177,6 +201,12 @@ RSpec.describe API::Namespaces do
expect(response).to have_gitlab_http_status(:unauthorized)
end
+
+ it 'returns authentication error' do
+ get api("/namespaces/#{project_namespace.id}")
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
end
context 'when authenticated as regular user' do
@@ -231,10 +261,10 @@ RSpec.describe API::Namespaces do
end
describe 'GET /namespaces/:namespace/exists' do
- let!(:namespace1) { create(:group, name: 'Namespace 1', path: 'namespace-1') }
- let!(:namespace2) { create(:group, name: 'Namespace 2', path: 'namespace-2') }
- let!(:namespace1sub) { create(:group, name: 'Sub Namespace 1', path: 'sub-namespace-1', parent: namespace1) }
- let!(:namespace2sub) { create(:group, name: 'Sub Namespace 2', path: 'sub-namespace-2', parent: namespace2) }
+ let_it_be(:namespace1) { create(:group, name: 'Namespace 1', path: 'namespace-1') }
+ let_it_be(:namespace2) { create(:group, name: 'Namespace 2', path: 'namespace-2') }
+ let_it_be(:namespace1sub) { create(:group, name: 'Sub Namespace 1', path: 'sub-namespace-1', parent: namespace1) }
+ let_it_be(:namespace2sub) { create(:group, name: 'Sub Namespace 2', path: 'sub-namespace-2', parent: namespace2) }
context 'when unauthenticated' do
it 'returns authentication error' do
@@ -242,6 +272,16 @@ RSpec.describe API::Namespaces do
expect(response).to have_gitlab_http_status(:unauthorized)
end
+
+ context 'when requesting project_namespace' do
+ let(:namespace_id) { project_namespace.id }
+
+ it 'returns authentication error' do
+ get api("/namespaces/#{project_namespace.path}/exists"), params: { parent_id: group2.id }
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
end
context 'when authenticated' do
@@ -300,6 +340,18 @@ RSpec.describe API::Namespaces do
expect(response).to have_gitlab_http_status(:ok)
expect(response.body).to eq(expected_json)
end
+
+ context 'when requesting project_namespace' do
+ let(:namespace_id) { project_namespace.id }
+
+ it 'returns JSON indicating the namespace does not exist without a suggestion' do
+ get api("/namespaces/#{project_namespace.path}/exists", user), params: { parent_id: group2.id }
+
+ expected_json = { exists: false, suggests: [] }.to_json
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to eq(expected_json)
+ end
+ end
end
end
end
diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb
index 0d04c2cad5b..7c3f1890095 100644
--- a/spec/requests/api/npm_project_packages_spec.rb
+++ b/spec/requests/api/npm_project_packages_spec.rb
@@ -180,6 +180,7 @@ RSpec.describe API::NpmProjectPackages do
.to change { project.packages.count }.by(1)
.and change { Packages::PackageFile.count }.by(1)
.and change { Packages::Tag.count }.by(1)
+ .and change { Packages::Npm::Metadatum.count }.by(1)
expect(response).to have_gitlab_http_status(:ok)
end
@@ -317,6 +318,25 @@ RSpec.describe API::NpmProjectPackages do
end
end
end
+
+ context 'with a too large metadata structure' do
+ let(:package_name) { "@#{group.path}/my_package_name" }
+ let(:params) do
+ upload_params(package_name: package_name, package_version: '1.2.3').tap do |h|
+ h['versions']['1.2.3']['test'] = 'test' * 10000
+ end
+ end
+
+ it_behaves_like 'not a package tracking event'
+
+ it 'returns an error' do
+ expect { upload_package_with_token }
+ .not_to change { project.packages.count }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response.body).to include('Validation failed: Package json structure is too large')
+ end
+ end
end
def upload_package(package_name, params = {})
diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml
index dd00d413664..01d2fb18f00 100644
--- a/spec/requests/api/project_attributes.yml
+++ b/spec/requests/api/project_attributes.yml
@@ -137,6 +137,7 @@ project_setting:
unexposed_attributes:
- created_at
- has_confluence
+ - has_shimo
- has_vulnerabilities
- prevent_merge_without_jira_issue
- warn_about_potentially_unwanted_characters
diff --git a/spec/requests/api/project_debian_distributions_spec.rb b/spec/requests/api/project_debian_distributions_spec.rb
index de7362758f7..2b993f24046 100644
--- a/spec/requests/api/project_debian_distributions_spec.rb
+++ b/spec/requests/api/project_debian_distributions_spec.rb
@@ -11,25 +11,31 @@ RSpec.describe API::ProjectDebianDistributions do
let(:url) { "/projects/#{container.id}/debian_distributions" }
let(:api_params) { { 'codename': 'my-codename' } }
- it_behaves_like 'Debian repository write endpoint', 'POST distribution request', :created, /^{.*"codename":"my-codename",.*"components":\["main"\],.*"architectures":\["all","amd64"\]/, authenticate_non_public: false
+ it_behaves_like 'Debian distributions write endpoint', 'POST', :created, /^{.*"codename":"my-codename",.*"components":\["main"\],.*"architectures":\["all","amd64"\]/
context 'with invalid parameters' do
let(:api_params) { { codename: distribution.codename } }
- it_behaves_like 'Debian repository write endpoint', 'GET request', :bad_request, /^{"message":{"codename":\["has already been taken"\]}}$/, authenticate_non_public: false
+ it_behaves_like 'Debian distributions write endpoint', 'GET', :bad_request, /^{"message":{"codename":\["has already been taken"\]}}$/
end
end
describe 'GET projects/:id/debian_distributions' do
let(:url) { "/projects/#{container.id}/debian_distributions" }
- it_behaves_like 'Debian repository read endpoint', 'GET request', :success, /^\[{.*"codename":"existing-codename\",.*"components":\["existing-component"\],.*"architectures":\["all","existing-arch"\]/, authenticate_non_public: false
+ it_behaves_like 'Debian distributions read endpoint', 'GET', :success, /^\[{.*"codename":"existing-codename\",.*"components":\["existing-component"\],.*"architectures":\["all","existing-arch"\]/
end
describe 'GET projects/:id/debian_distributions/:codename' do
let(:url) { "/projects/#{container.id}/debian_distributions/#{distribution.codename}" }
- it_behaves_like 'Debian repository read endpoint', 'GET request', :success, /^{.*"codename":"existing-codename\",.*"components":\["existing-component"\],.*"architectures":\["all","existing-arch"\]/, authenticate_non_public: false
+ it_behaves_like 'Debian distributions read endpoint', 'GET', :success, /^{.*"codename":"existing-codename\",.*"components":\["existing-component"\],.*"architectures":\["all","existing-arch"\]/
+ end
+
+ describe 'GET projects/:id/debian_distributions/:codename/key.asc' do
+ let(:url) { "/projects/#{container.id}/debian_distributions/#{distribution.codename}/key.asc" }
+
+ it_behaves_like 'Debian distributions read endpoint', 'GET', :success, /^-----BEGIN PGP PUBLIC KEY BLOCK-----/
end
describe 'PUT projects/:id/debian_distributions/:codename' do
@@ -37,12 +43,12 @@ RSpec.describe API::ProjectDebianDistributions do
let(:url) { "/projects/#{container.id}/debian_distributions/#{distribution.codename}" }
let(:api_params) { { suite: 'my-suite' } }
- it_behaves_like 'Debian repository write endpoint', 'PUT distribution request', :success, /^{.*"codename":"existing-codename",.*"suite":"my-suite",/, authenticate_non_public: false
+ it_behaves_like 'Debian distributions write endpoint', 'PUT', :success, /^{.*"codename":"existing-codename",.*"suite":"my-suite",/
context 'with invalid parameters' do
let(:api_params) { { suite: distribution.codename } }
- it_behaves_like 'Debian repository write endpoint', 'GET request', :bad_request, /^{"message":{"suite":\["has already been taken as Codename"\]}}$/, authenticate_non_public: false
+ it_behaves_like 'Debian distributions write endpoint', 'GET', :bad_request, /^{"message":{"suite":\["has already been taken as Codename"\]}}$/
end
end
@@ -50,7 +56,7 @@ RSpec.describe API::ProjectDebianDistributions do
let(:method) { :delete }
let(:url) { "/projects/#{container.id}/debian_distributions/#{distribution.codename}" }
- it_behaves_like 'Debian repository maintainer write endpoint', 'DELETE distribution request', :success, /^{\"message\":\"202 Accepted\"}$/, authenticate_non_public: false
+ it_behaves_like 'Debian distributions maintainer write endpoint', 'DELETE', :success, /^{\"message\":\"202 Accepted\"}$/
context 'when destroy fails' do
before do
@@ -59,7 +65,7 @@ RSpec.describe API::ProjectDebianDistributions do
end
end
- it_behaves_like 'Debian repository maintainer write endpoint', 'GET request', :bad_request, /^{"message":"Failed to delete distribution"}$/, authenticate_non_public: false
+ it_behaves_like 'Debian distributions maintainer write endpoint', 'GET', :bad_request, /^{"message":"Failed to delete distribution"}$/
end
end
end
diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb
index 0c9e125cc90..097d374640c 100644
--- a/spec/requests/api/project_import_spec.rb
+++ b/spec/requests/api/project_import_spec.rb
@@ -47,7 +47,7 @@ RSpec.describe API::ProjectImport do
it 'executes a limited number of queries' do
control_count = ActiveRecord::QueryRecorder.new { subject }.count
- expect(control_count).to be <= 100
+ expect(control_count).to be <= 101
end
it 'schedules an import using a namespace' do
diff --git a/spec/requests/api/project_snapshots_spec.rb b/spec/requests/api/project_snapshots_spec.rb
index f23e374407b..33c86d56ed4 100644
--- a/spec/requests/api/project_snapshots_spec.rb
+++ b/spec/requests/api/project_snapshots_spec.rb
@@ -29,6 +29,7 @@ RSpec.describe API::ProjectSnapshots do
repository: repository.gitaly_repository
).to_json
)
+ expect(response.parsed_body).to be_empty
end
it 'returns authentication error as project owner' do
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
index 8cd1f15a88d..512cbf7c321 100644
--- a/spec/requests/api/project_snippets_spec.rb
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -400,6 +400,7 @@ RSpec.describe API::ProjectSnippets do
expect(response).to have_gitlab_http_status(:ok)
expect(response.media_type).to eq 'text/plain'
+ expect(response.parsed_body).to be_empty
end
it 'returns 404 for invalid snippet id' do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index dd6afa869e0..4f84e6f2562 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -48,6 +48,7 @@ end
RSpec.describe API::Projects do
include ProjectForksHelper
+ include StubRequests
let_it_be(:user) { create(:user) }
let_it_be(:user2) { create(:user) }
@@ -358,7 +359,7 @@ RSpec.describe API::Projects do
statistics = json_response.find { |p| p['id'] == project.id }['statistics']
expect(statistics).to be_present
- expect(statistics).to include('commit_count', 'storage_size', 'repository_size', 'wiki_size', 'lfs_objects_size', 'job_artifacts_size', 'snippets_size', 'packages_size')
+ expect(statistics).to include('commit_count', 'storage_size', 'repository_size', 'wiki_size', 'lfs_objects_size', 'job_artifacts_size', 'pipeline_artifacts_size', 'snippets_size', 'packages_size', 'uploads_size')
end
it "does not include license by default" do
@@ -1159,6 +1160,34 @@ RSpec.describe API::Projects do
expect(response).to have_gitlab_http_status(:forbidden)
end
+ it 'disallows creating a project with an import_url that is not reachable', :aggregate_failures do
+ url = 'http://example.com'
+ endpoint_url = "#{url}/info/refs?service=git-upload-pack"
+ stub_full_request(endpoint_url, method: :get).to_return({ status: 301, body: '', headers: nil })
+ project_params = { import_url: url, path: 'path-project-Foo', name: 'Foo Project' }
+
+ expect { post api('/projects', user), params: project_params }.not_to change { Project.count }
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ expect(json_response['message']).to eq("#{url} is not a valid HTTP Git repository")
+ end
+
+ it 'creates a project with an import_url that is valid', :aggregate_failures do
+ url = 'http://example.com'
+ endpoint_url = "#{url}/info/refs?service=git-upload-pack"
+ git_response = {
+ status: 200,
+ body: '001e# service=git-upload-pack',
+ headers: { 'Content-Type': 'application/x-git-upload-pack-advertisement' }
+ }
+ stub_full_request(endpoint_url, method: :get).to_return(git_response)
+ project_params = { import_url: url, path: 'path-project-Foo', name: 'Foo Project' }
+
+ expect { post api('/projects', user), params: project_params }.to change { Project.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(:created)
+ end
+
it 'sets a project as public' do
project = attributes_for(:project, visibility: 'public')
diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb
index 90b03a480a8..cb9b6a072b1 100644
--- a/spec/requests/api/releases_spec.rb
+++ b/spec/requests/api/releases_spec.rb
@@ -42,6 +42,14 @@ RSpec.describe API::Releases do
expect(response).to have_gitlab_http_status(:ok)
end
+ it 'returns 200 HTTP status when using JOB-TOKEN auth' do
+ job = create(:ci_build, :running, project: project, user: maintainer)
+
+ get api("/projects/#{project.id}/releases"), params: { job_token: job.token }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
it 'returns releases ordered by released_at' do
get api("/projects/#{project.id}/releases", maintainer)
@@ -316,6 +324,14 @@ RSpec.describe API::Releases do
expect(response).to have_gitlab_http_status(:ok)
end
+ it 'returns 200 HTTP status when using JOB-TOKEN auth' do
+ job = create(:ci_build, :running, project: project, user: maintainer)
+
+ get api("/projects/#{project.id}/releases/v0.1"), params: { job_token: job.token }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
it 'returns a release entry' do
get api("/projects/#{project.id}/releases/v0.1", maintainer)
@@ -1008,6 +1024,14 @@ RSpec.describe API::Releases do
expect(response).to have_gitlab_http_status(:ok)
end
+ it 'accepts the request when using JOB-TOKEN auth' do
+ job = create(:ci_build, :running, project: project, user: maintainer)
+
+ put api("/projects/#{project.id}/releases/v0.1"), params: params.merge(job_token: job.token)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
it 'updates the description' do
put api("/projects/#{project.id}/releases/v0.1", maintainer), params: params
@@ -1220,6 +1244,14 @@ RSpec.describe API::Releases do
expect(response).to have_gitlab_http_status(:ok)
end
+ it 'accepts the request when using JOB-TOKEN auth' do
+ job = create(:ci_build, :running, project: project, user: maintainer)
+
+ delete api("/projects/#{project.id}/releases/v0.1"), params: { job_token: job.token }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
it 'destroys the release' do
expect do
delete api("/projects/#{project.id}/releases/v0.1", maintainer)
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index f05f125c974..f3146480be2 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -197,6 +197,7 @@ RSpec.describe API::Repositories do
expect(response).to have_gitlab_http_status(:ok)
expect(headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
+ expect(response.parsed_body).to be_empty
end
it 'sets inline content disposition by default' do
@@ -274,6 +275,7 @@ RSpec.describe API::Repositories do
expect(type).to eq('git-archive')
expect(params['ArchivePath']).to match(/#{project.path}\-[^\.]+\.tar.gz/)
+ expect(response.parsed_body).to be_empty
end
it 'returns the repository archive archive.zip' do
@@ -495,6 +497,43 @@ RSpec.describe API::Repositories do
expect(response).to have_gitlab_http_status(:not_found)
end
+
+ it "returns a newly created commit", :use_clean_rails_redis_caching do
+ # Parse the commits ourselves because json_response is cached
+ def commit_messages(response)
+ Gitlab::Json.parse(response.body)["commits"].map do |commit|
+ commit["message"]
+ end
+ end
+
+ # First trigger the rate limit cache
+ get api(route, current_user), params: { from: 'master', to: 'feature' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(commit_messages(response)).not_to include("Cool new commit")
+
+ # Then create a new commit via the API
+ post api("/projects/#{project.id}/repository/commits", user), params: {
+ branch: "feature",
+ commit_message: "Cool new commit",
+ actions: [
+ {
+ action: "create",
+ file_path: "foo/bar/baz.txt",
+ content: "puts 8"
+ }
+ ]
+ }
+
+ expect(response).to have_gitlab_http_status(:created)
+
+ # Now perform the same query as before, but the cache should have expired
+ # and our new commit should exist
+ get api(route, current_user), params: { from: 'master', to: 'feature' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(commit_messages(response)).to include("Cool new commit")
+ end
end
context 'when unauthenticated', 'and project is public' do
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 423e19c3971..641c6a2cd91 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -612,5 +612,46 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
expect(json_response.slice(*settings.keys)).to eq(settings)
end
end
+
+ context 'Sentry settings' do
+ let(:settings) do
+ {
+ sentry_enabled: true,
+ sentry_dsn: 'http://sentry.example.com',
+ sentry_clientside_dsn: 'http://sentry.example.com',
+ sentry_environment: 'production'
+ }
+ end
+
+ let(:attribute_names) { settings.keys.map(&:to_s) }
+
+ it 'includes the attributes in the API' do
+ get api('/application/settings', admin)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ attribute_names.each do |attribute|
+ expect(json_response.keys).to include(attribute)
+ end
+ end
+
+ it 'allows updating the settings' do
+ put api('/application/settings', admin), params: settings
+
+ expect(response).to have_gitlab_http_status(:ok)
+ settings.each do |attribute, value|
+ expect(ApplicationSetting.current.public_send(attribute)).to eq(value)
+ end
+ end
+
+ context 'missing sentry_dsn value when sentry_enabled is true' do
+ it 'returns a blank parameter error message' do
+ put api('/application/settings', admin), params: { sentry_enabled: true }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ message = json_response['message']
+ expect(message["sentry_dsn"]).to include(a_string_matching("can't be blank"))
+ end
+ end
+ end
end
end
diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb
index f4d15d0525e..dd5e6ac8a5e 100644
--- a/spec/requests/api/snippets_spec.rb
+++ b/spec/requests/api/snippets_spec.rb
@@ -113,6 +113,7 @@ RSpec.describe API::Snippets, factory_default: :keep do
expect(response).to have_gitlab_http_status(:ok)
expect(response.media_type).to eq 'text/plain'
expect(headers['Content-Disposition']).to match(/^inline/)
+ expect(response.parsed_body).to be_empty
end
it 'returns 404 for invalid snippet id' do
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index 1aa1ad87be9..bb56192a2ff 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -17,6 +17,10 @@ RSpec.describe API::Tags do
end
describe 'GET /projects/:id/repository/tags' do
+ before do
+ stub_feature_flags(tag_list_keyset_pagination: false)
+ end
+
shared_examples "get repository tags" do
let(:route) { "/projects/#{project_id}/repository/tags" }
@@ -143,6 +147,55 @@ RSpec.describe API::Tags do
expect(expected_tag['release']['description']).to eq(description)
end
end
+
+ context 'with keyset pagination on', :aggregate_errors do
+ before do
+ stub_feature_flags(tag_list_keyset_pagination: true)
+ end
+
+ context 'with keyset pagination option' do
+ let(:base_params) { { pagination: 'keyset' } }
+
+ context 'with gitaly pagination params' do
+ context 'with high limit' do
+ let(:params) { base_params.merge(per_page: 100) }
+
+ it 'returns all repository tags' do
+ get api(route, user), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('public_api/v4/tags')
+ expect(response.headers).not_to include('Link')
+ tag_names = json_response.map { |x| x['name'] }
+ expect(tag_names).to match_array(project.repository.tag_names)
+ end
+ end
+
+ context 'with low limit' do
+ let(:params) { base_params.merge(per_page: 2) }
+
+ it 'returns limited repository tags' do
+ get api(route, user), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('public_api/v4/tags')
+ expect(response.headers).to include('Link')
+ tag_names = json_response.map { |x| x['name'] }
+ expect(tag_names).to match_array(%w(v1.1.0 v1.1.1))
+ end
+ end
+
+ context 'with missing page token' do
+ let(:params) { base_params.merge(page_token: 'unknown') }
+
+ it_behaves_like '422 response' do
+ let(:request) { get api(route, user), params: params }
+ let(:message) { 'Invalid page token: refs/tags/unknown' }
+ end
+ end
+ end
+ end
+ end
end
context ":api_caching_tags flag enabled", :use_clean_rails_memory_store_caching do
@@ -208,6 +261,20 @@ RSpec.describe API::Tags do
it_behaves_like "get repository tags"
end
+
+ context 'when gitaly is unavailable' do
+ let(:route) { "/projects/#{project_id}/repository/tags" }
+
+ before do
+ expect_next_instance_of(TagsFinder) do |finder|
+ allow(finder).to receive(:execute).and_raise(Gitlab::Git::CommandError)
+ end
+ end
+
+ it_behaves_like '503 response' do
+ let(:request) { get api(route, user) }
+ end
+ end
end
describe 'GET /projects/:id/repository/tags/:tag_name' do
diff --git a/spec/requests/api/terraform/modules/v1/packages_spec.rb b/spec/requests/api/terraform/modules/v1/packages_spec.rb
index b04f5ad9a94..b17bc11a451 100644
--- a/spec/requests/api/terraform/modules/v1/packages_spec.rb
+++ b/spec/requests/api/terraform/modules/v1/packages_spec.rb
@@ -28,10 +28,25 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
describe 'GET /api/v4/packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/versions' do
let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/versions") }
- let(:headers) { {} }
+ let(:headers) { { 'Authorization' => "Bearer #{tokens[:job_token]}" } }
subject { get(url, headers: headers) }
+ context 'with a conflicting package name' do
+ let!(:conflicting_package) { create(:terraform_module_package, project: project, name: "conflict-#{package.name}", version: '2.0.0') }
+
+ before do
+ group.add_developer(user)
+ end
+
+ it 'returns only one version' do
+ subject
+
+ expect(json_response['modules'][0]['versions'].size).to eq(1)
+ expect(json_response['modules'][0]['versions'][0]['version']).to eq('1.0.0')
+ end
+ end
+
context 'with valid namespace' do
where(:visibility, :user_role, :member, :token_type, :valid_token, :shared_examples_name, :expected_status) do
:public | :developer | true | :personal_access_token | true | 'returns terraform module packages' | :success
diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb
index d31f571e636..c9deb84ff98 100644
--- a/spec/requests/api/todos_spec.rb
+++ b/spec/requests/api/todos_spec.rb
@@ -13,6 +13,8 @@ RSpec.describe API::Todos do
let_it_be(:john_doe) { create(:user, username: 'john_doe') }
let_it_be(:issue) { create(:issue, project: project_1) }
let_it_be(:merge_request) { create(:merge_request, source_project: project_1) }
+ let_it_be(:alert) { create(:alert_management_alert, project: project_1) }
+ let_it_be(:alert_todo) { create(:todo, project: project_1, author: john_doe, user: john_doe, target: alert) }
let_it_be(:merge_request_todo) { create(:todo, project: project_1, author: author_2, user: john_doe, target: merge_request) }
let_it_be(:pending_1) { create(:todo, :mentioned, project: project_1, author: author_1, user: john_doe, target: issue) }
let_it_be(:pending_2) { create(:todo, project: project_2, author: author_2, user: john_doe, target: issue) }
@@ -67,7 +69,7 @@ RSpec.describe API::Todos do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.length).to eq(4)
+ expect(json_response.length).to eq(5)
expect(json_response[0]['id']).to eq(pending_3.id)
expect(json_response[0]['project']).to be_a Hash
expect(json_response[0]['author']).to be_a Hash
@@ -95,6 +97,10 @@ RSpec.describe API::Todos do
expect(json_response[3]['target']['merge_requests_count']).to be_nil
expect(json_response[3]['target']['upvotes']).to eq(1)
expect(json_response[3]['target']['downvotes']).to eq(0)
+
+ expect(json_response[4]['target_type']).to eq('AlertManagement::Alert')
+ expect(json_response[4]['target']['iid']).to eq(alert.iid)
+ expect(json_response[4]['target']['title']).to eq(alert.title)
end
context "when current user does not have access to one of the TODO's target" do
@@ -105,7 +111,7 @@ RSpec.describe API::Todos do
get api('/todos', john_doe)
- expect(json_response.count).to eq(4)
+ expect(json_response.count).to eq(5)
expect(json_response.map { |t| t['id'] }).not_to include(no_access_todo.id, pending_4.id)
end
end
@@ -163,7 +169,7 @@ RSpec.describe API::Todos do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.length).to eq(3)
+ expect(json_response.length).to eq(4)
end
end
diff --git a/spec/requests/api/topics_spec.rb b/spec/requests/api/topics_spec.rb
new file mode 100644
index 00000000000..a5746a4022e
--- /dev/null
+++ b/spec/requests/api/topics_spec.rb
@@ -0,0 +1,217 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Topics do
+ include WorkhorseHelpers
+
+ let_it_be(:topic_1) { create(:topic, name: 'Git', total_projects_count: 1) }
+ let_it_be(:topic_2) { create(:topic, name: 'GitLab', total_projects_count: 2) }
+ let_it_be(:topic_3) { create(:topic, name: 'other-topic', total_projects_count: 3) }
+
+ let_it_be(:admin) { create(:user, :admin) }
+ let_it_be(:user) { create(:user) }
+
+ let(:file) { fixture_file_upload('spec/fixtures/dk.png') }
+
+ describe 'GET /topics', :aggregate_failures do
+ it 'returns topics ordered by total_projects_count' do
+ get api('/topics')
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+
+ expect(json_response[0]['id']).to eq(topic_3.id)
+ expect(json_response[0]['name']).to eq('other-topic')
+ expect(json_response[0]['total_projects_count']).to eq(3)
+
+ expect(json_response[1]['id']).to eq(topic_2.id)
+ expect(json_response[1]['name']).to eq('GitLab')
+ expect(json_response[1]['total_projects_count']).to eq(2)
+
+ expect(json_response[2]['id']).to eq(topic_1.id)
+ expect(json_response[2]['name']).to eq('Git')
+ expect(json_response[2]['total_projects_count']).to eq(1)
+ end
+
+ context 'with search' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:search, :result) do
+ '' | %w[other-topic GitLab Git]
+ 'g' | %w[]
+ 'gi' | %w[]
+ 'git' | %w[Git GitLab]
+ 'x' | %w[]
+ 0 | %w[]
+ end
+
+ with_them do
+ it 'returns filtered topics' do
+ get api('/topics'), params: { search: search }
+
+ expect(json_response.map { |t| t['name'] }).to eq(result)
+ end
+ end
+ end
+
+ context 'with pagination' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:params, :result) do
+ { page: 0 } | %w[other-topic GitLab Git]
+ { page: 1 } | %w[other-topic GitLab Git]
+ { page: 2 } | %w[]
+ { per_page: 1 } | %w[other-topic]
+ { per_page: 2 } | %w[other-topic GitLab]
+ { per_page: 3 } | %w[other-topic GitLab Git]
+ { page: 0, per_page: 1 } | %w[other-topic]
+ { page: 0, per_page: 2 } | %w[other-topic GitLab]
+ { page: 1, per_page: 1 } | %w[other-topic]
+ { page: 1, per_page: 2 } | %w[other-topic GitLab]
+ { page: 2, per_page: 1 } | %w[GitLab]
+ { page: 2, per_page: 2 } | %w[Git]
+ { page: 3, per_page: 1 } | %w[Git]
+ { page: 3, per_page: 2 } | %w[]
+ { page: 4, per_page: 1 } | %w[]
+ { page: 4, per_page: 2 } | %w[]
+ end
+
+ with_them do
+ it 'returns paginated topics' do
+ get api('/topics'), params: params
+
+ expect(json_response.map { |t| t['name'] }).to eq(result)
+ end
+ end
+ end
+ end
+
+ describe 'GET /topic/:id', :aggregate_failures do
+ it 'returns topic' do
+ get api("/topics/#{topic_2.id}")
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ expect(json_response['id']).to eq(topic_2.id)
+ expect(json_response['name']).to eq('GitLab')
+ expect(json_response['total_projects_count']).to eq(2)
+ end
+
+ it 'returns 404 for non existing id' do
+ get api("/topics/#{non_existing_record_id}")
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'returns 400 for invalid `id` parameter' do
+ get api('/topics/invalid')
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eql('id is invalid')
+ end
+ end
+
+ describe 'POST /topics', :aggregate_failures do
+ context 'as administrator' do
+ it 'creates a topic' do
+ post api('/topics/', admin), params: { name: 'my-topic' }
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['name']).to eq('my-topic')
+ expect(Projects::Topic.find(json_response['id']).name).to eq('my-topic')
+ end
+
+ it 'creates a topic with avatar and description' do
+ workhorse_form_with_file(
+ api('/topics/', admin),
+ file_key: :avatar,
+ params: { name: 'my-topic', description: 'my description...', avatar: file }
+ )
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['description']).to eq('my description...')
+ expect(json_response['avatar_url']).to end_with('dk.png')
+ end
+
+ it 'returns 400 if name is missing' do
+ post api('/topics/', admin)
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eql('name is missing')
+ end
+ end
+
+ context 'as normal user' do
+ it 'returns 403 Forbidden' do
+ post api('/topics/', user), params: { name: 'my-topic' }
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'as anonymous' do
+ it 'returns 401 Unauthorized' do
+ post api('/topics/'), params: { name: 'my-topic' }
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+ end
+
+ describe 'PUT /topics', :aggregate_failures do
+ context 'as administrator' do
+ it 'updates a topic' do
+ put api("/topics/#{topic_3.id}", admin), params: { name: 'my-topic' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['name']).to eq('my-topic')
+ expect(topic_3.reload.name).to eq('my-topic')
+ end
+
+ it 'updates a topic with avatar and description' do
+ workhorse_form_with_file(
+ api("/topics/#{topic_3.id}", admin),
+ method: :put,
+ file_key: :avatar,
+ params: { description: 'my description...', avatar: file }
+ )
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['description']).to eq('my description...')
+ expect(json_response['avatar_url']).to end_with('dk.png')
+ end
+
+ it 'returns 404 for non existing id' do
+ put api("/topics/#{non_existing_record_id}", admin), params: { name: 'my-topic' }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'returns 400 for invalid `id` parameter' do
+ put api('/topics/invalid', admin), params: { name: 'my-topic' }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eql('id is invalid')
+ end
+ end
+
+ context 'as normal user' do
+ it 'returns 403 Forbidden' do
+ put api("/topics/#{topic_3.id}", user), params: { name: 'my-topic' }
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'as anonymous' do
+ it 'returns 401 Unauthorized' do
+ put api("/topics/#{topic_3.id}"), params: { name: 'my-topic' }
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index fb01845b63a..b93df2f3bae 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -1464,6 +1464,7 @@ RSpec.describe API::Users do
credit_card_expiration_year: expiration_year,
credit_card_expiration_month: 1,
credit_card_holder_name: 'John Smith',
+ credit_card_type: 'AmericanExpress',
credit_card_mask_number: '1111'
}
end
@@ -1495,6 +1496,7 @@ RSpec.describe API::Users do
credit_card_validated_at: credit_card_validated_time,
expiration_date: Date.new(expiration_year, 1, 31),
last_digits: 1111,
+ network: 'AmericanExpress',
holder_name: 'John Smith'
)
end
@@ -1904,7 +1906,8 @@ RSpec.describe API::Users 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.first['email']).to eq(email.email)
+ expect(json_response.first['email']).to eq(user.email)
+ expect(json_response.second['email']).to eq(email.email)
end
it "returns a 404 for invalid ID" do
@@ -2486,7 +2489,8 @@ RSpec.describe API::Users 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.first["email"]).to eq(email.email)
+ expect(json_response.first['email']).to eq(user.email)
+ expect(json_response.second['email']).to eq(email.email)
end
context "scopes" do
diff --git a/spec/requests/api/v3/github_spec.rb b/spec/requests/api/v3/github_spec.rb
index 255f53e4c7c..6d8ae226ce4 100644
--- a/spec/requests/api/v3/github_spec.rb
+++ b/spec/requests/api/v3/github_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe API::V3::Github do
let_it_be(:user) { create(:user) }
let_it_be(:unauthorized_user) { create(:user) }
let_it_be(:admin) { create(:user, :admin) }
- let_it_be(:project) { create(:project, :repository, creator: user) }
+ let_it_be_with_reload(:project) { create(:project, :repository, creator: user) }
before do
project.add_maintainer(user)
@@ -506,11 +506,18 @@ RSpec.describe API::V3::Github do
describe 'GET /repos/:namespace/:project/commits/:sha' do
let(:commit) { project.repository.commit }
- let(:commit_id) { commit.id }
+
+ def call_api(commit_id: commit.id)
+ jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/commits/#{commit_id}", user)
+ end
+
+ def response_diff_files(response)
+ Gitlab::Json.parse(response.body)['files']
+ end
context 'authenticated' do
- it 'returns commit with github format' do
- jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/commits/#{commit_id}", user)
+ it 'returns commit with github format', :aggregate_failures do
+ call_api
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('entities/github/commit')
@@ -519,36 +526,130 @@ RSpec.describe API::V3::Github do
it 'returns 200 when project path include a dot' do
project.update!(path: 'foo.bar')
- jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/commits/#{commit_id}", user)
+ call_api
expect(response).to have_gitlab_http_status(:ok)
end
- it 'returns 200 when namespace path include a dot' do
- group = create(:group, path: 'foo.bar')
- project = create(:project, :repository, group: group)
- project.add_reporter(user)
+ context 'when namespace path includes a dot' do
+ let(:group) { create(:group, path: 'foo.bar') }
+ let(:project) { create(:project, :repository, group: group) }
- jira_get v3_api("/repos/#{group.path}/#{project.path}/commits/#{commit_id}", user)
+ it 'returns 200 when namespace path include a dot' do
+ project.add_reporter(user)
- expect(response).to have_gitlab_http_status(:ok)
+ call_api
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when the Gitaly `CommitDiff` RPC times out', :use_clean_rails_memory_store_caching do
+ let(:commit_diff_args) { [project.repository_storage, :diff_service, :commit_diff, any_args] }
+
+ before do
+ allow(Gitlab::GitalyClient).to receive(:call)
+ .and_call_original
+ end
+
+ it 'handles the error, logs it, and returns empty diff files', :aggregate_failures do
+ allow(Gitlab::GitalyClient).to receive(:call)
+ .with(*commit_diff_args)
+ .and_raise(GRPC::DeadlineExceeded)
+
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception)
+ .with an_instance_of(GRPC::DeadlineExceeded)
+
+ call_api
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response_diff_files(response)).to be_blank
+ end
+
+ it 'does not handle the error when feature flag is disabled', :aggregate_failures do
+ stub_feature_flags(api_v3_commits_skip_diff_files: false)
+
+ allow(Gitlab::GitalyClient).to receive(:call)
+ .with(*commit_diff_args)
+ .and_raise(GRPC::DeadlineExceeded)
+
+ call_api
+
+ expect(response).to have_gitlab_http_status(:error)
+ end
+
+ it 'only calls Gitaly once for all attempts within a period of time', :aggregate_failures do
+ expect(Gitlab::GitalyClient).to receive(:call)
+ .with(*commit_diff_args)
+ .once # <- once
+ .and_raise(GRPC::DeadlineExceeded)
+
+ 3.times do
+ call_api
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response_diff_files(response)).to be_blank
+ end
+ end
+
+ it 'calls Gitaly again after a period of time', :aggregate_failures do
+ expect(Gitlab::GitalyClient).to receive(:call)
+ .with(*commit_diff_args)
+ .twice # <- twice
+ .and_raise(GRPC::DeadlineExceeded)
+
+ call_api
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response_diff_files(response)).to be_blank
+
+ travel_to((described_class::GITALY_TIMEOUT_CACHE_EXPIRY + 1.second).from_now) do
+ call_api
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response_diff_files(response)).to be_blank
+ end
+ end
+
+ it 'uses a unique cache key, allowing other calls to succeed' do
+ cache_key = [described_class::GITALY_TIMEOUT_CACHE_KEY, project.id, commit.cache_key].join(':')
+ Rails.cache.write(cache_key, 1)
+
+ expect(Gitlab::GitalyClient).to receive(:call)
+ .with(*commit_diff_args)
+ .once # <- once
+
+ call_api
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response_diff_files(response)).to be_blank
+
+ call_api(commit_id: commit.parent.id)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response_diff_files(response).length).to eq(1)
+ end
end
end
context 'unauthenticated' do
+ let(:user) { nil }
+
it 'returns 401' do
- jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/commits/#{commit_id}", nil)
+ call_api
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
context 'unauthorized' do
+ let(:user) { unauthorized_user }
+
it 'returns 404 when lower access level' do
- project.add_guest(unauthorized_user)
+ project.add_guest(user)
- jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/commits/#{commit_id}",
- unauthorized_user)
+ call_api
expect(response).to have_gitlab_http_status(:not_found)
end
diff --git a/spec/requests/groups/email_campaigns_controller_spec.rb b/spec/requests/groups/email_campaigns_controller_spec.rb
index 4d630ef6710..9ed828d1a9a 100644
--- a/spec/requests/groups/email_campaigns_controller_spec.rb
+++ b/spec/requests/groups/email_campaigns_controller_spec.rb
@@ -94,7 +94,7 @@ RSpec.describe Groups::EmailCampaignsController do
describe 'track parameter' do
context 'when valid' do
- where(track: Namespaces::InProductMarketingEmailsService::TRACKS.keys.without(:experience))
+ where(track: [Namespaces::InProductMarketingEmailsService::TRACKS.keys.without(:experience), Namespaces::InviteTeamEmailService::TRACK].flatten)
with_them do
it_behaves_like 'track and redirect'
@@ -117,6 +117,10 @@ RSpec.describe Groups::EmailCampaignsController do
with_them do
it_behaves_like 'track and redirect'
end
+
+ it_behaves_like 'track and redirect' do
+ let(:track) { Namespaces::InviteTeamEmailService::TRACK.to_s }
+ end
end
context 'when invalid' do
@@ -124,6 +128,10 @@ RSpec.describe Groups::EmailCampaignsController do
with_them do
it_behaves_like 'no track and 404'
+
+ it_behaves_like 'no track and 404' do
+ let(:track) { Namespaces::InviteTeamEmailService::TRACK.to_s }
+ end
end
end
end
diff --git a/spec/requests/groups/settings/applications_controller_spec.rb b/spec/requests/groups/settings/applications_controller_spec.rb
new file mode 100644
index 00000000000..74313491414
--- /dev/null
+++ b/spec/requests/groups/settings/applications_controller_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Groups::Settings::ApplicationsController do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:application) { create(:oauth_application, owner_id: group.id, owner_type: 'Namespace') }
+ let_it_be(:show_path) { group_settings_application_path(group, application) }
+ let_it_be(:create_path) { group_settings_applications_path(group) }
+
+ before do
+ sign_in(user)
+ group.add_owner(user)
+ end
+
+ include_examples 'applications controller - GET #show'
+
+ include_examples 'applications controller - POST #create'
+end
diff --git a/spec/requests/import/gitlab_groups_controller_spec.rb b/spec/requests/import/gitlab_groups_controller_spec.rb
index 1f6487986a3..4abf99cf994 100644
--- a/spec/requests/import/gitlab_groups_controller_spec.rb
+++ b/spec/requests/import/gitlab_groups_controller_spec.rb
@@ -60,6 +60,7 @@ RSpec.describe Import::GitlabGroupsController do
end
it 'imports the group data', :sidekiq_inline do
+ allow(GroupImportWorker).to receive(:with_status).and_return(GroupImportWorker)
allow(GroupImportWorker).to receive(:perform_async).and_call_original
import_request
@@ -67,7 +68,6 @@ RSpec.describe Import::GitlabGroupsController do
group = Group.find_by(name: 'test-group-import')
expect(GroupImportWorker).to have_received(:perform_async).with(user.id, group.id)
-
expect(group.description).to eq 'A voluptate non sequi temporibus quam at.'
expect(group.visibility_level).to eq Gitlab::VisibilityLevel::PRIVATE
end
diff --git a/spec/requests/jwks_controller_spec.rb b/spec/requests/jwks_controller_spec.rb
index 5eda1979027..6dbb5988f58 100644
--- a/spec/requests/jwks_controller_spec.rb
+++ b/spec/requests/jwks_controller_spec.rb
@@ -3,6 +3,20 @@
require 'spec_helper'
RSpec.describe JwksController do
+ describe 'Endpoints from the parent Doorkeeper::OpenidConnect::DiscoveryController' do
+ it 'respond successfully' do
+ [
+ "/oauth/discovery/keys",
+ "/.well-known/openid-configuration",
+ "/.well-known/webfinger?resource=#{create(:user).email}"
+ ].each do |endpoint|
+ get endpoint
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+
describe 'GET /-/jwks' do
let(:ci_jwt_signing_key) { OpenSSL::PKey::RSA.generate(1024) }
let(:ci_jwk) { ci_jwt_signing_key.to_jwk }
diff --git a/spec/requests/oauth/applications_controller_spec.rb b/spec/requests/oauth/applications_controller_spec.rb
new file mode 100644
index 00000000000..78f0cedb56f
--- /dev/null
+++ b/spec/requests/oauth/applications_controller_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Oauth::ApplicationsController do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:application) { create(:oauth_application, owner: user) }
+ let_it_be(:show_path) { oauth_application_path(application) }
+ let_it_be(:create_path) { oauth_applications_path }
+
+ before do
+ sign_in(user)
+ end
+
+ include_examples 'applications controller - GET #show'
+
+ include_examples 'applications controller - POST #create'
+end
diff --git a/spec/requests/projects/google_cloud_controller_spec.rb b/spec/requests/projects/google_cloud_controller_spec.rb
index 3b43f0d1dfb..37682152994 100644
--- a/spec/requests/projects/google_cloud_controller_spec.rb
+++ b/spec/requests/projects/google_cloud_controller_spec.rb
@@ -2,48 +2,106 @@
require 'spec_helper'
+# Mock Types
+MockGoogleOAuth2Credentials = Struct.new(:app_id, :app_secret)
+
RSpec.describe Projects::GoogleCloudController do
let_it_be(:project) { create(:project, :public) }
describe 'GET index' do
let_it_be(:url) { "#{project_google_cloud_index_path(project)}" }
- let(:subject) { get url }
+ context 'when a public request is made' do
+ it 'returns not found' do
+ get url
- context 'when user is authorized' do
- let(:user) { project.creator }
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
- before do
+ context 'when a project.guest makes request' do
+ let(:user) { create(:user) }
+
+ it 'returns not found' do
+ project.add_guest(user)
sign_in(user)
- subject
+
+ get url
+
+ expect(response).to have_gitlab_http_status(:not_found)
end
+ end
- it 'renders content' do
- expect(response).to be_successful
+ context 'when project.developer makes request' do
+ let(:user) { create(:user) }
+
+ it 'returns not found' do
+ project.add_developer(user)
+ sign_in(user)
+
+ get url
+
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
- context 'when user is unauthorized' do
+ context 'when project.maintainer makes request' do
let(:user) { create(:user) }
- before do
- project.add_guest(user)
+ it 'returns successful' do
+ project.add_maintainer(user)
sign_in(user)
- subject
+
+ get url
+
+ expect(response).to be_successful
end
+ end
- it 'shows 404' do
- expect(response).to have_gitlab_http_status(:not_found)
+ context 'when project.creator makes request' do
+ let(:user) { project.creator }
+
+ it 'returns successful' do
+ sign_in(user)
+
+ get url
+
+ expect(response).to be_successful
end
end
- context 'when no user is present' do
- before do
- subject
+ describe 'when authorized user makes request' do
+ let(:user) { project.creator }
+
+ context 'but gitlab instance is not configured for google oauth2' do
+ before do
+ unconfigured_google_oauth2 = MockGoogleOAuth2Credentials.new('', '')
+ allow(Gitlab::Auth::OAuth::Provider).to receive(:config_for)
+ .with('google_oauth2')
+ .and_return(unconfigured_google_oauth2)
+ end
+
+ it 'returns forbidden' do
+ sign_in(user)
+
+ get url
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
end
- it 'shows 404' do
- expect(response).to have_gitlab_http_status(:not_found)
+ context 'but feature flag is disabled' do
+ before do
+ stub_feature_flags(incubation_5mp_google_cloud: false)
+ end
+
+ it 'returns not found' do
+ sign_in(user)
+
+ get url
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
end
end
end
diff --git a/spec/requests/projects/issues/discussions_spec.rb b/spec/requests/projects/issues/discussions_spec.rb
new file mode 100644
index 00000000000..dcdca2d9c27
--- /dev/null
+++ b/spec/requests/projects/issues/discussions_spec.rb
@@ -0,0 +1,115 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'issue discussions' do
+ describe 'GET /:namespace/:project/-/issues/:iid/discussions' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:note_author) { create(:user) }
+ let_it_be(:notes) { create_list(:note, 5, project: project, noteable: issue, author: note_author) }
+
+ before_all do
+ project.add_maintainer(user)
+ end
+
+ context 'HTTP caching' do
+ def get_discussions
+ get discussions_namespace_project_issue_path(namespace_id: project.namespace, project_id: project, id: issue.iid), headers: {
+ 'If-None-Match' => @etag
+ }
+
+ @etag = response.etag
+ end
+
+ before do
+ sign_in(user)
+
+ get_discussions
+ end
+
+ it 'returns 304 without serializing JSON' do
+ expect(DiscussionSerializer).not_to receive(:new)
+
+ get_discussions
+
+ expect(response).to have_gitlab_http_status(:not_modified)
+ end
+
+ shared_examples 'cache miss' do
+ it 'returns 200 and serializes JSON' do
+ expect(DiscussionSerializer).to receive(:new).and_call_original
+
+ get_discussions
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when user role changes' do
+ before do
+ project.add_guest(user)
+ end
+
+ it_behaves_like 'cache miss'
+ end
+
+ context 'when emoji is awarded to a note' do
+ before do
+ travel_to(1.minute.from_now) { create(:award_emoji, awardable: notes.first) }
+ end
+
+ it_behaves_like 'cache miss'
+ end
+
+ context 'when note author name changes' do
+ before do
+ note_author.update!(name: 'New name')
+ end
+
+ it_behaves_like 'cache miss'
+ end
+
+ context 'when note author status changes' do
+ before do
+ Users::SetStatusService.new(note_author, message: "updated status").execute
+ end
+
+ it_behaves_like 'cache miss'
+ end
+
+ context 'when note author role changes' do
+ before do
+ project.add_developer(note_author)
+ end
+
+ it_behaves_like 'cache miss'
+ end
+
+ context 'when note is added' do
+ before do
+ create(:note, project: project, noteable: issue)
+ end
+
+ it_behaves_like 'cache miss'
+ end
+
+ context 'when note is modified' do
+ before do
+ notes.first.update!(note: 'edited text')
+ end
+
+ it_behaves_like 'cache miss'
+ end
+
+ context 'when note is deleted' do
+ before do
+ notes.first.destroy!
+ end
+
+ it_behaves_like 'cache miss'
+ end
+ end
+ end
+end
diff --git a/spec/requests/projects/issues_controller_spec.rb b/spec/requests/projects/issues_controller_spec.rb
new file mode 100644
index 00000000000..f44b1f4d502
--- /dev/null
+++ b/spec/requests/projects/issues_controller_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::IssuesController do
+ let_it_be(:issue) { create(:issue) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { issue.project }
+ let_it_be(:user) { issue.author }
+
+ before do
+ login_as(user)
+ end
+
+ describe 'GET #discussions' do
+ let_it_be(:discussion) { create(:discussion_note_on_issue, noteable: issue, project: issue.project) }
+ let_it_be(:discussion_reply) { create(:discussion_note_on_issue, noteable: issue, project: issue.project, in_reply_to: discussion) }
+ let_it_be(:state_event) { create(:resource_state_event, issue: issue) }
+ let_it_be(:discussion_2) { create(:discussion_note_on_issue, noteable: issue, project: issue.project) }
+ let_it_be(:discussion_3) { create(:discussion_note_on_issue, noteable: issue, project: issue.project) }
+
+ context 'pagination' do
+ def get_discussions(**params)
+ get discussions_project_issue_path(project, issue, params: params.merge(format: :json))
+ end
+
+ it 'returns paginated notes and cursor based on per_page param' do
+ get_discussions(per_page: 2)
+
+ discussions = Gitlab::Json.parse(response.body)
+ notes = discussions.flat_map { |d| d['notes'] }
+
+ expect(discussions.count).to eq(2)
+ expect(notes).to match([
+ a_hash_including('id' => discussion.id.to_s),
+ a_hash_including('id' => discussion_reply.id.to_s),
+ a_hash_including('type' => 'StateNote')
+ ])
+
+ cursor = response.header['X-Next-Page-Cursor']
+ expect(cursor).to be_present
+
+ get_discussions(per_page: 1, cursor: cursor)
+
+ discussions = Gitlab::Json.parse(response.body)
+ notes = discussions.flat_map { |d| d['notes'] }
+
+ expect(discussions.count).to eq(1)
+ expect(notes).to match([
+ a_hash_including('id' => discussion_2.id.to_s)
+ ])
+ end
+
+ context 'when paginated_issue_discussions is disabled' do
+ before do
+ stub_feature_flags(paginated_issue_discussions: false)
+ end
+
+ it 'returns all discussions and ignores per_page param' do
+ get_discussions(per_page: 2)
+
+ discussions = Gitlab::Json.parse(response.body)
+ notes = discussions.flat_map { |d| d['notes'] }
+
+ expect(discussions.count).to eq(4)
+ expect(notes.count).to eq(5)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/projects/usage_quotas_spec.rb b/spec/requests/projects/usage_quotas_spec.rb
index 04e01da61ef..114e9bd9f1e 100644
--- a/spec/requests/projects/usage_quotas_spec.rb
+++ b/spec/requests/projects/usage_quotas_spec.rb
@@ -22,40 +22,26 @@ RSpec.describe 'Project Usage Quotas' do
end
describe 'GET /:namespace/:project/usage_quotas' do
- context 'with project_storage_ui feature flag enabled' do
- before do
- stub_feature_flags(project_storage_ui: true)
- end
-
- it 'renders usage quotas path' do
- mock_storage_app_data = {
- project_path: project.full_path,
- usage_quotas_help_page_path: help_page_path('user/usage_quotas'),
- build_artifacts_help_page_path: help_page_path('ci/pipelines/job_artifacts', anchor: 'when-job-artifacts-are-deleted'),
- packages_help_page_path: help_page_path('user/packages/package_registry/index.md', anchor: 'delete-a-package'),
- repository_help_page_path: help_page_path('user/project/repository/reducing_the_repo_size_using_git'),
- snippets_help_page_path: help_page_path('user/snippets', anchor: 'reduce-snippets-repository-size'),
- wiki_help_page_path: help_page_path('administration/wikis/index.md', anchor: 'reduce-wiki-repository-size')
- }
- get project_usage_quotas_path(project)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.body).to include(project_usage_quotas_path(project))
- expect(assigns[:storage_app_data]).to eq(mock_storage_app_data)
- expect(response.body).to include("Usage of project resources across the <strong>#{project.name}</strong> project")
- end
-
- context 'renders :not_found for user without permission' do
- let(:role) { :developer }
-
- it_behaves_like 'response with 404 status'
- end
+ it 'renders usage quotas path' do
+ mock_storage_app_data = {
+ project_path: project.full_path,
+ usage_quotas_help_page_path: help_page_path('user/usage_quotas'),
+ build_artifacts_help_page_path: help_page_path('ci/pipelines/job_artifacts', anchor: 'when-job-artifacts-are-deleted'),
+ packages_help_page_path: help_page_path('user/packages/package_registry/index.md', anchor: 'delete-a-package'),
+ repository_help_page_path: help_page_path('user/project/repository/reducing_the_repo_size_using_git'),
+ snippets_help_page_path: help_page_path('user/snippets', anchor: 'reduce-snippets-repository-size'),
+ wiki_help_page_path: help_page_path('administration/wikis/index.md', anchor: 'reduce-wiki-repository-size')
+ }
+ get project_usage_quotas_path(project)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to include(project_usage_quotas_path(project))
+ expect(assigns[:storage_app_data]).to eq(mock_storage_app_data)
+ expect(response.body).to include("Usage of project resources across the <strong>#{project.name}</strong> project")
end
- context 'with project_storage_ui feature flag disabled' do
- before do
- stub_feature_flags(project_storage_ui: false)
- end
+ context 'renders :not_found for user without permission' do
+ let(:role) { :developer }
it_behaves_like 'response with 404 status'
end
diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb
index 35ce942ed7e..ab0c76397e4 100644
--- a/spec/requests/rack_attack_global_spec.rb
+++ b/spec/requests/rack_attack_global_spec.rb
@@ -517,11 +517,15 @@ RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_cac
let(:path) { "/v2/#{group.path}/dependency_proxy/containers/alpine/manifests/latest" }
let(:other_path) { "/v2/#{other_group.path}/dependency_proxy/containers/alpine/manifests/latest" }
let(:pull_response) { { status: :success, manifest: manifest, from_cache: false } }
+ let(:head_response) { { status: :success } }
before do
allow_next_instance_of(DependencyProxy::FindOrCreateManifestService) do |instance|
allow(instance).to receive(:execute).and_return(pull_response)
end
+ allow_next_instance_of(DependencyProxy::HeadManifestService) do |instance|
+ allow(instance).to receive(:execute).and_return(head_response)
+ end
end
it_behaves_like 'rate-limited token-authenticated requests'
diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb
index accacd705e7..701a73761fd 100644
--- a/spec/requests/users_controller_spec.rb
+++ b/spec/requests/users_controller_spec.rb
@@ -305,7 +305,7 @@ RSpec.describe UsersController do
context 'user with keys' do
let!(:gpg_key) { create(:gpg_key, user: user) }
- let!(:another_gpg_key) { create(:another_gpg_key, user: user) }
+ let!(:another_gpg_key) { create(:another_gpg_key, user: user.reload) }
shared_examples_for 'renders all verified GPG keys' do
it 'renders all verified keys separated with a new line with text/plain content type' do
diff --git a/spec/routing/group_routing_spec.rb b/spec/routing/group_routing_spec.rb
index f171c2faf5e..5c2ef62683e 100644
--- a/spec/routing/group_routing_spec.rb
+++ b/spec/routing/group_routing_spec.rb
@@ -85,6 +85,26 @@ RSpec.describe "Groups", "routing" do
expect(get('/v2')).to route_to('groups/dependency_proxy_auth#authenticate')
end
+ it 'routes to #upload_manifest' do
+ expect(post('v2/gitlabhq/dependency_proxy/containers/alpine/manifests/latest/upload'))
+ .to route_to('groups/dependency_proxy_for_containers#upload_manifest', group_id: 'gitlabhq', image: 'alpine', tag: 'latest')
+ end
+
+ it 'routes to #upload_blob' do
+ expect(post('v2/gitlabhq/dependency_proxy/containers/alpine/blobs/abc12345/upload'))
+ .to route_to('groups/dependency_proxy_for_containers#upload_blob', group_id: 'gitlabhq', image: 'alpine', sha: 'abc12345')
+ end
+
+ it 'routes to #upload_manifest_authorize' do
+ expect(post('v2/gitlabhq/dependency_proxy/containers/alpine/manifests/latest/upload/authorize'))
+ .to route_to('groups/dependency_proxy_for_containers#authorize_upload_manifest', group_id: 'gitlabhq', image: 'alpine', tag: 'latest')
+ end
+
+ it 'routes to #upload_blob_authorize' do
+ expect(post('v2/gitlabhq/dependency_proxy/containers/alpine/blobs/abc12345/upload/authorize'))
+ .to route_to('groups/dependency_proxy_for_containers#authorize_upload_blob', group_id: 'gitlabhq', image: 'alpine', sha: 'abc12345')
+ end
+
context 'image name without namespace' do
it 'routes to #manifest' do
expect(get('/v2/gitlabhq/dependency_proxy/containers/ruby/manifests/2.3.6'))
diff --git a/spec/routing/openid_connect_spec.rb b/spec/routing/openid_connect_spec.rb
index dc9190114fd..4c08a71ae31 100644
--- a/spec/routing/openid_connect_spec.rb
+++ b/spec/routing/openid_connect_spec.rb
@@ -2,20 +2,20 @@
require 'spec_helper'
-# oauth_discovery_keys GET /oauth/discovery/keys(.:format) doorkeeper/openid_connect/discovery#keys
-# oauth_discovery_provider GET /.well-known/openid-configuration(.:format) doorkeeper/openid_connect/discovery#provider
-# oauth_discovery_webfinger GET /.well-known/webfinger(.:format) doorkeeper/openid_connect/discovery#webfinger
+# oauth_discovery_keys GET /oauth/discovery/keys(.:format) jwks#keys
+# oauth_discovery_provider GET /.well-known/openid-configuration(.:format) jwks#provider
+# oauth_discovery_webfinger GET /.well-known/webfinger(.:format) jwks#webfinger
RSpec.describe Doorkeeper::OpenidConnect::DiscoveryController, 'routing' do
it "to #provider" do
- expect(get('/.well-known/openid-configuration')).to route_to('doorkeeper/openid_connect/discovery#provider')
+ expect(get('/.well-known/openid-configuration')).to route_to('jwks#provider')
end
it "to #webfinger" do
- expect(get('/.well-known/webfinger')).to route_to('doorkeeper/openid_connect/discovery#webfinger')
+ expect(get('/.well-known/webfinger')).to route_to('jwks#webfinger')
end
it "to #keys" do
- expect(get('/oauth/discovery/keys')).to route_to('doorkeeper/openid_connect/discovery#keys')
+ expect(get('/oauth/discovery/keys')).to route_to('jwks#keys')
end
end
diff --git a/spec/rubocop/cop/gitlab/bulk_insert_spec.rb b/spec/rubocop/cop/gitlab/bulk_insert_spec.rb
index bbc8f381d01..7cd003d0a70 100644
--- a/spec/rubocop/cop/gitlab/bulk_insert_spec.rb
+++ b/spec/rubocop/cop/gitlab/bulk_insert_spec.rb
@@ -6,17 +6,17 @@ require_relative '../../../../rubocop/cop/gitlab/bulk_insert'
RSpec.describe RuboCop::Cop::Gitlab::BulkInsert do
subject(:cop) { described_class.new }
- it 'flags the use of Gitlab::Database.main.bulk_insert' do
+ it 'flags the use of ApplicationRecord.legacy_bulk_insert' do
expect_offense(<<~SOURCE)
- Gitlab::Database.main.bulk_insert('merge_request_diff_files', rows)
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use the `BulkInsertSafe` concern, [...]
+ ApplicationRecord.legacy_bulk_insert('merge_request_diff_files', rows)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use the `BulkInsertSafe` concern, [...]
SOURCE
end
- it 'flags the use of ::Gitlab::Database.main.bulk_insert' do
+ it 'flags the use of ::ApplicationRecord.legacy_bulk_insert' do
expect_offense(<<~SOURCE)
- ::Gitlab::Database.main.bulk_insert('merge_request_diff_files', rows)
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use the `BulkInsertSafe` concern, [...]
+ ::ApplicationRecord.legacy_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 f3c07e44cc7..ff6365aa0f7 100644
--- a/spec/rubocop/cop/gitlab/change_timezone_spec.rb
+++ b/spec/rubocop/cop/gitlab/change_timezone_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'fast_spec_helper'
-require_relative '../../../../rubocop/cop/gitlab/change_timzone'
+require_relative '../../../../rubocop/cop/gitlab/change_timezone'
RSpec.describe RuboCop::Cop::Gitlab::ChangeTimezone do
subject(:cop) { described_class.new }
diff --git a/spec/rubocop/cop/qa/duplicate_testcase_link_spec.rb b/spec/rubocop/cop/qa/duplicate_testcase_link_spec.rb
new file mode 100644
index 00000000000..fb424da90e8
--- /dev/null
+++ b/spec/rubocop/cop/qa/duplicate_testcase_link_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+require_relative '../../../../rubocop/cop/qa/duplicate_testcase_link'
+
+RSpec.describe RuboCop::Cop::QA::DuplicateTestcaseLink do
+ let(:source_file) { 'qa/page.rb' }
+
+ subject(:cop) { described_class.new }
+
+ context 'in a QA file' do
+ before do
+ allow(cop).to receive(:in_qa_file?).and_return(true)
+ end
+
+ it "registers an offense for a duplicate testcase link" do
+ expect_offense(<<-RUBY)
+ it 'some test', testcase: '/quality/test_cases/1892' do
+ end
+ it 'another test', testcase: '/quality/test_cases/1892' do
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't reuse the same testcase link in different tests. Replace one of `/quality/test_cases/1892`.
+ end
+ RUBY
+ end
+
+ it "doesnt offend if testcase link is unique" do
+ expect_no_offenses(<<-RUBY)
+ it 'some test', testcase: '/quality/test_cases/1893' do
+ end
+ it 'another test', testcase: '/quality/test_cases/1894' do
+ end
+ RUBY
+ end
+ end
+end
diff --git a/spec/scripts/changed-feature-flags_spec.rb b/spec/scripts/changed-feature-flags_spec.rb
new file mode 100644
index 00000000000..5c858588c0c
--- /dev/null
+++ b/spec/scripts/changed-feature-flags_spec.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+load File.expand_path('../../scripts/changed-feature-flags', __dir__)
+
+RSpec.describe 'scripts/changed-feature-flags' do
+ describe GetFeatureFlagsFromFiles do
+ let(:feature_flag_definition1) do
+ file = Tempfile.new('foo.yml', ff_dir)
+ file.write(<<~YAML)
+ ---
+ name: foo_flag
+ default_enabled: true
+ YAML
+ file.rewind
+ file
+ end
+
+ let(:feature_flag_definition2) do
+ file = Tempfile.new('bar.yml', ff_dir)
+ file.write(<<~YAML)
+ ---
+ name: bar_flag
+ default_enabled: false
+ YAML
+ file.rewind
+ file
+ end
+
+ after do
+ FileUtils.remove_entry(ff_dir, true)
+ end
+
+ describe '.extracted_flags' do
+ shared_examples 'extract feature flags' do
+ it 'returns feature flags on their own' do
+ subject = described_class.new({ files: [feature_flag_definition1.path, feature_flag_definition2.path] })
+
+ expect(subject.extracted_flags).to eq('foo_flag,bar_flag')
+ end
+
+ it 'returns feature flags and their state as enabled' do
+ subject = described_class.new({ files: [feature_flag_definition1.path, feature_flag_definition2.path], state: 'enabled' })
+
+ expect(subject.extracted_flags).to eq('foo_flag=enabled,bar_flag=enabled')
+ end
+
+ it 'returns feature flags and their state as disabled' do
+ subject = described_class.new({ files: [feature_flag_definition1.path, feature_flag_definition2.path], state: 'disabled' })
+
+ expect(subject.extracted_flags).to eq('foo_flag=disabled,bar_flag=disabled')
+ end
+ end
+
+ context 'with definition files in the development directory' do
+ let(:ff_dir) { FileUtils.mkdir_p(File.join(Dir.tmpdir, 'feature_flags', 'development')) }
+
+ it_behaves_like 'extract feature flags'
+ end
+
+ context 'with definition files in the ops directory' do
+ let(:ff_dir) { FileUtils.mkdir_p(File.join(Dir.tmpdir, 'feature_flags', 'ops')) }
+
+ it_behaves_like 'extract feature flags'
+ end
+
+ context 'with definition files in the experiment directory' do
+ let(:ff_dir) { FileUtils.mkdir_p(File.join(Dir.tmpdir, 'feature_flags', 'experiment')) }
+
+ it 'ignores the files' do
+ subject = described_class.new({ files: [feature_flag_definition1.path, feature_flag_definition2.path] })
+
+ expect(subject.extracted_flags).to eq('')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/scripts/failed_tests_spec.rb b/spec/scripts/failed_tests_spec.rb
new file mode 100644
index 00000000000..92eae75b3be
--- /dev/null
+++ b/spec/scripts/failed_tests_spec.rb
@@ -0,0 +1,127 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_relative '../../scripts/failed_tests'
+
+RSpec.describe FailedTests do
+ let(:report_file) { 'spec/fixtures/scripts/test_report.json' }
+ let(:output_directory) { 'tmp/previous_test_results' }
+ let(:rspec_pg_regex) { /rspec .+ pg12( .+)?/ }
+ let(:rspec_ee_pg_regex) { /rspec-ee .+ pg12( .+)?/ }
+
+ subject { described_class.new(previous_tests_report_path: report_file, output_directory: output_directory, rspec_pg_regex: rspec_pg_regex, rspec_ee_pg_regex: rspec_ee_pg_regex) }
+
+ describe '#output_failed_test_files' do
+ it 'writes the file for the suite' do
+ expect(File).to receive(:open).with(File.join(output_directory, "rspec_failed_files.txt"), 'w').once
+
+ subject.output_failed_test_files
+ end
+ end
+
+ describe '#failed_files_for_suite_collection' do
+ let(:failure_path) { 'path/to/fail_file_spec.rb' }
+ let(:other_failure_path) { 'path/to/fail_file_spec_2.rb' }
+ let(:file_contents_as_json) do
+ {
+ 'suites' => [
+ {
+ 'failed_count' => 1,
+ 'name' => 'rspec unit pg12 10/12',
+ 'test_cases' => [
+ {
+ 'status' => 'failed',
+ 'file' => failure_path
+ }
+ ]
+ },
+ {
+ 'failed_count' => 1,
+ 'name' => 'rspec-ee unit pg12',
+ 'test_cases' => [
+ {
+ 'status' => 'failed',
+ 'file' => failure_path
+ }
+ ]
+ },
+ {
+ 'failed_count' => 1,
+ 'name' => 'rspec unit pg13 10/12',
+ 'test_cases' => [
+ {
+ 'status' => 'failed',
+ 'file' => other_failure_path
+ }
+ ]
+ }
+ ]
+ }
+ end
+
+ before do
+ allow(subject).to receive(:file_contents_as_json).and_return(file_contents_as_json)
+ end
+
+ it 'returns a list of failed file paths for suite collection' do
+ result = subject.failed_files_for_suite_collection
+
+ expect(result[:rspec].to_a).to match_array(failure_path)
+ expect(result[:rspec_ee].to_a).to match_array(failure_path)
+ end
+ end
+
+ describe 'empty report' do
+ let(:file_content) do
+ '{}'
+ end
+
+ before do
+ allow(subject).to receive(:file_contents).and_return(file_content)
+ end
+
+ it 'does not fail for output files' do
+ subject.output_failed_test_files
+ end
+
+ it 'returns empty results for suite failures' do
+ result = subject.failed_files_for_suite_collection
+
+ expect(result.values.flatten).to be_empty
+ end
+ end
+
+ describe 'invalid report' do
+ let(:file_content) do
+ ''
+ end
+
+ before do
+ allow(subject).to receive(:file_contents).and_return(file_content)
+ end
+
+ it 'does not fail for output files' do
+ subject.output_failed_test_files
+ end
+
+ it 'returns empty results for suite failures' do
+ result = subject.failed_files_for_suite_collection
+
+ expect(result.values.flatten).to be_empty
+ end
+ end
+
+ describe 'missing report file' do
+ let(:report_file) { 'unknownfile.json' }
+
+ it 'does not fail for output files' do
+ subject.output_failed_test_files
+ end
+
+ it 'returns empty results for suite failures' do
+ result = subject.failed_files_for_suite_collection
+
+ expect(result.values.flatten).to be_empty
+ end
+ end
+end
diff --git a/spec/scripts/pipeline_test_report_builder_spec.rb b/spec/scripts/pipeline_test_report_builder_spec.rb
new file mode 100644
index 00000000000..8553ada044e
--- /dev/null
+++ b/spec/scripts/pipeline_test_report_builder_spec.rb
@@ -0,0 +1,185 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_relative '../../scripts/pipeline_test_report_builder'
+
+RSpec.describe PipelineTestReportBuilder do
+ let(:report_file) { 'spec/fixtures/scripts/test_report.json' }
+ let(:output_file_path) { 'tmp/previous_test_results/output_file.json' }
+
+ subject do
+ described_class.new(
+ target_project: 'gitlab-org/gitlab',
+ mr_id: '999',
+ instance_base_url: 'https://gitlab.com',
+ output_file_path: output_file_path
+ )
+ end
+
+ let(:failed_pipeline_url) { 'pipeline2_url' }
+
+ let(:failed_pipeline) do
+ {
+ 'status' => 'failed',
+ 'created_at' => (DateTime.now - 5).to_s,
+ 'web_url' => failed_pipeline_url
+ }
+ end
+
+ let(:current_pipeline) do
+ {
+ 'status' => 'running',
+ 'created_at' => DateTime.now.to_s,
+ 'web_url' => 'pipeline1_url'
+ }
+ end
+
+ let(:mr_pipelines) { [current_pipeline, failed_pipeline] }
+
+ let(:failed_build_id) { 9999 }
+
+ let(:failed_builds_for_pipeline) do
+ [
+ {
+ 'id' => failed_build_id,
+ 'stage' => 'test'
+ }
+ ]
+ end
+
+ let(:test_report_for_build) do
+ {
+ "name": "rspec-ee system pg11 geo",
+ "failed_count": 41,
+ "test_cases": [
+ {
+ "status": "failed",
+ "name": "example",
+ "classname": "ee.spec.features.geo_node_spec",
+ "file": "./ee/spec/features/geo_node_spec.rb",
+ "execution_time": 6.324748,
+ "system_output": {
+ "__content__": "\n",
+ "message": "RSpec::Core::MultipleExceptionError",
+ "type": "RSpec::Core::MultipleExceptionError"
+ }
+ }
+ ]
+ }
+ end
+
+ before do
+ allow(subject).to receive(:pipelines_for_mr).and_return(mr_pipelines)
+ allow(subject).to receive(:failed_builds_for_pipeline).and_return(failed_builds_for_pipeline)
+ end
+
+ describe '#previous_pipeline' do
+ let(:fork_pipeline_url) { 'fork_pipeline_url' }
+ let(:fork_pipeline) do
+ {
+ 'status' => 'failed',
+ 'created_at' => (DateTime.now - 5).to_s,
+ 'web_url' => fork_pipeline_url
+ }
+ end
+
+ before do
+ allow(subject).to receive(:test_report_for_build).and_return(test_report_for_build)
+ end
+
+ context 'pipeline in a fork project' do
+ let(:mr_pipelines) { [current_pipeline, fork_pipeline] }
+
+ it 'returns fork pipeline' do
+ expect(subject.previous_pipeline).to eq(fork_pipeline)
+ end
+ end
+
+ context 'pipeline in target project' do
+ it 'returns failed pipeline' do
+ expect(subject.previous_pipeline).to eq(failed_pipeline)
+ end
+ end
+ end
+
+ describe '#test_report_for_latest_pipeline' do
+ it 'fetches builds from pipeline related to MR' do
+ expect(subject).to receive(:fetch).with("#{failed_pipeline_url}/tests/suite.json?build_ids[]=#{failed_build_id}").and_return(failed_builds_for_pipeline)
+ subject.test_report_for_latest_pipeline
+ end
+
+ context 'canonical pipeline' do
+ before do
+ allow(subject).to receive(:test_report_for_build).and_return(test_report_for_build)
+ end
+
+ context 'no previous pipeline' do
+ let(:mr_pipelines) { [] }
+
+ it 'returns empty hash' do
+ expect(subject.test_report_for_latest_pipeline).to eq("{}")
+ end
+ end
+
+ context 'first pipeline scenario' do
+ let(:mr_pipelines) do
+ [
+ {
+ 'status' => 'running',
+ 'created_at' => DateTime.now.to_s
+ }
+ ]
+ end
+
+ it 'returns empty hash' do
+ expect(subject.test_report_for_latest_pipeline).to eq("{}")
+ end
+ end
+
+ context 'no previous failed pipeline' do
+ let(:mr_pipelines) do
+ [
+ {
+ 'status' => 'running',
+ 'created_at' => DateTime.now.to_s
+ },
+ {
+ 'status' => 'success',
+ 'created_at' => (DateTime.now - 5).to_s
+ }
+ ]
+ end
+
+ it 'returns empty hash' do
+ expect(subject.test_report_for_latest_pipeline).to eq("{}")
+ end
+ end
+
+ context 'no failed test builds' do
+ let(:failed_builds_for_pipeline) do
+ [
+ {
+ 'id' => 9999,
+ 'stage' => 'prepare'
+ }
+ ]
+ end
+
+ it 'returns empty hash' do
+ expect(subject.test_report_for_latest_pipeline).to eq("{}")
+ end
+ end
+
+ context 'failed pipeline and failed test builds' do
+ it 'returns populated test list for suites' do
+ actual = subject.test_report_for_latest_pipeline
+ expected = {
+ 'suites' => [test_report_for_build]
+ }.to_json
+
+ expect(actual).to eq(expected)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/serializers/analytics_summary_serializer_spec.rb b/spec/serializers/analytics_summary_serializer_spec.rb
index 9429c9d571a..6563b58c334 100644
--- a/spec/serializers/analytics_summary_serializer_spec.rb
+++ b/spec/serializers/analytics_summary_serializer_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe AnalyticsSummarySerializer do
context 'when representing with unit' do
let(:resource) do
Gitlab::CycleAnalytics::Summary::DeploymentFrequency
- .new(deployments: 10, options: { from: 1.day.ago })
+ .new(deployments: 10, options: { from: 1.day.ago }, project: project)
end
subject { described_class.new.represent(resource, with_unit: true) }
diff --git a/spec/serializers/merge_request_user_entity_spec.rb b/spec/serializers/merge_request_user_entity_spec.rb
index 026a229322e..72d1b0c0dd2 100644
--- a/spec/serializers/merge_request_user_entity_spec.rb
+++ b/spec/serializers/merge_request_user_entity_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe MergeRequestUserEntity do
let_it_be(:user) { create(:user) }
- let_it_be(:merge_request) { create(:merge_request) }
+ let_it_be(:merge_request) { create(:merge_request, assignees: [user]) }
let(:request) { EntityRequest.new(project: merge_request.target_project, current_user: user) }
@@ -18,7 +18,8 @@ RSpec.describe MergeRequestUserEntity do
it 'exposes needed attributes' do
is_expected.to include(
:id, :name, :username, :state, :avatar_url, :web_url,
- :can_merge, :can_update_merge_request, :reviewed, :approved
+ :can_merge, :can_update_merge_request, :reviewed, :approved,
+ :attention_requested
)
end
@@ -56,6 +57,10 @@ RSpec.describe MergeRequestUserEntity do
end
end
+ context 'attention_requested' do
+ it { is_expected.to include(attention_requested: true ) }
+ end
+
describe 'performance' do
let_it_be(:user_a) { create(:user) }
let_it_be(:user_b) { create(:user) }
diff --git a/spec/serializers/merge_request_widget_entity_spec.rb b/spec/serializers/merge_request_widget_entity_spec.rb
index fcfdbfc0967..3e0c61a26c0 100644
--- a/spec/serializers/merge_request_widget_entity_spec.rb
+++ b/spec/serializers/merge_request_widget_entity_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe MergeRequestWidgetEntity do
include ProjectForksHelper
+ include Gitlab::Routing.url_helpers
let(:project) { create :project, :repository }
let(:resource) { create(:merge_request, source_project: project, target_project: project) }
@@ -140,17 +141,15 @@ RSpec.describe MergeRequestWidgetEntity do
let(:role) { :developer }
it 'has add ci config path' do
- expected_path = "/#{resource.project.full_path}/-/new/#{resource.source_branch}"
+ expected_path = project_ci_pipeline_editor_path(project)
expect(subject[:merge_request_add_ci_config_path]).to include(expected_path)
end
it 'has expected params' do
expected_params = {
- commit_message: 'Add .gitlab-ci.yml',
- file_name: '.gitlab-ci.yml',
- suggest_gitlab_ci_yml: 'true',
- mr_path: "/#{resource.project.full_path}/-/merge_requests/#{resource.iid}"
+ branch_name: resource.source_branch,
+ add_new_config_file: 'true'
}.with_indifferent_access
uri = Addressable::URI.parse(subject[:merge_request_add_ci_config_path])
@@ -188,30 +187,6 @@ RSpec.describe MergeRequestWidgetEntity do
end
end
- context 'when ci_config_path is customized' do
- it 'has no path if ci_config_path is not set to our default setting' do
- project.ci_config_path = 'not_default'
-
- expect(subject[:merge_request_add_ci_config_path]).to be_nil
- end
-
- it 'has a path if ci_config_path unset' do
- expect(subject[:merge_request_add_ci_config_path]).not_to be_nil
- end
-
- it 'has a path if ci_config_path is an empty string' do
- project.ci_config_path = ''
-
- expect(subject[:merge_request_add_ci_config_path]).not_to be_nil
- end
-
- it 'has a path if ci_config_path is set to our default file' do
- project.ci_config_path = Gitlab::FileDetector::PATTERNS[:gitlab_ci]
-
- expect(subject[:merge_request_add_ci_config_path]).not_to be_nil
- end
- end
-
context 'when build feature is disabled' do
before do
project.project_feature.update!(builds_access_level: ProjectFeature::DISABLED)
diff --git a/spec/serializers/service_field_entity_spec.rb b/spec/serializers/service_field_entity_spec.rb
index 6e9ebfb66d9..a06fdf95159 100644
--- a/spec/serializers/service_field_entity_spec.rb
+++ b/spec/serializers/service_field_entity_spec.rb
@@ -27,7 +27,8 @@ RSpec.describe ServiceFieldEntity do
help: 'Use a username for server version and an email for cloud version.',
required: true,
choices: nil,
- value: 'jira_username'
+ value: 'jira_username',
+ checkbox_label: nil
}
is_expected.to eq(expected_hash)
@@ -46,7 +47,8 @@ RSpec.describe ServiceFieldEntity do
help: 'Leave blank to use your current password or API token.',
required: true,
choices: nil,
- value: 'true'
+ value: 'true',
+ checkbox_label: nil
}
is_expected.to eq(expected_hash)
@@ -68,7 +70,8 @@ RSpec.describe ServiceFieldEntity do
placeholder: nil,
required: nil,
choices: nil,
- value: 'true'
+ value: 'true',
+ checkbox_label: nil
}
is_expected.to include(expected_hash)
@@ -83,12 +86,13 @@ RSpec.describe ServiceFieldEntity do
expected_hash = {
type: 'select',
name: 'branches_to_be_notified',
- title: nil,
+ title: 'Branches for which notifications are to be sent',
placeholder: nil,
required: nil,
choices: [['All branches', 'all'], ['Default branch', 'default'], ['Protected branches', 'protected'], ['Default branch and protected branches', 'default_and_protected']],
help: nil,
- value: nil
+ value: nil,
+ checkbox_label: nil
}
is_expected.to eq(expected_hash)
diff --git a/spec/services/admin/propagate_integration_service_spec.rb b/spec/services/admin/propagate_integration_service_spec.rb
index 151658fe429..b379286ba4f 100644
--- a/spec/services/admin/propagate_integration_service_spec.rb
+++ b/spec/services/admin/propagate_integration_service_spec.rb
@@ -55,7 +55,7 @@ RSpec.describe Admin::PropagateIntegrationService do
end
context 'for a group-level integration' do
- let(:group_integration) { create(:jira_integration, group: group, project: nil) }
+ let(:group_integration) { create(:jira_integration, :group, group: group) }
context 'with a project without integration' do
let(:another_project) { create(:project, group: group) }
@@ -81,7 +81,7 @@ RSpec.describe Admin::PropagateIntegrationService do
context 'with a subgroup with integration' do
let(:subgroup) { create(:group, parent: group) }
- let(:subgroup_integration) { create(:jira_integration, group: subgroup, project: nil, inherit_from_id: group_integration.id) }
+ let(:subgroup_integration) { create(:jira_integration, :group, group: subgroup, inherit_from_id: group_integration.id) }
it 'calls to PropagateIntegrationInheritDescendantWorker' do
expect(PropagateIntegrationInheritDescendantWorker).to receive(:perform_async)
diff --git a/spec/services/authorized_project_update/project_access_changed_service_spec.rb b/spec/services/authorized_project_update/project_access_changed_service_spec.rb
new file mode 100644
index 00000000000..11621055a47
--- /dev/null
+++ b/spec/services/authorized_project_update/project_access_changed_service_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe AuthorizedProjectUpdate::ProjectAccessChangedService do
+ describe '#execute' do
+ it 'schedules the project IDs' do
+ expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to receive(:bulk_perform_and_wait)
+ .with([[1], [2]])
+
+ described_class.new([1, 2]).execute
+ end
+
+ it 'permits non-blocking operation' do
+ expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to receive(:bulk_perform_async)
+ .with([[1], [2]])
+
+ described_class.new([1, 2]).execute(blocking: false)
+ end
+ end
+end
diff --git a/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb
index eaa5f723bec..6f28f892f00 100644
--- a/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb
+++ b/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb
@@ -24,6 +24,10 @@ RSpec.describe AutoMerge::MergeWhenPipelineSucceedsService do
project.add_maintainer(user)
end
+ before do
+ allow(MergeWorker).to receive(:with_status).and_return(MergeWorker)
+ end
+
describe "#available_for?" do
subject { service.available_for?(mr_merge_if_green_enabled) }
diff --git a/spec/services/award_emojis/base_service_spec.rb b/spec/services/award_emojis/base_service_spec.rb
new file mode 100644
index 00000000000..e0c8fd39ad9
--- /dev/null
+++ b/spec/services/award_emojis/base_service_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe AwardEmojis::BaseService do
+ let(:awardable) { build(:note) }
+ let(:current_user) { build(:user) }
+
+ describe '.initialize' do
+ subject { described_class }
+
+ it 'uses same emoji name if not an alias' do
+ emoji_name = 'horse'
+
+ expect(subject.new(awardable, emoji_name, current_user).name).to eq(emoji_name)
+ end
+
+ it 'uses emoji original name if its an alias' do
+ emoji_alias = 'small_airplane'
+ emoji_name = 'airplane_small'
+
+ expect(subject.new(awardable, emoji_alias, current_user).name).to eq(emoji_name)
+ end
+ end
+end
diff --git a/spec/services/bulk_create_integration_service_spec.rb b/spec/services/bulk_create_integration_service_spec.rb
index 517222c0e69..63bdc39857c 100644
--- a/spec/services/bulk_create_integration_service_spec.rb
+++ b/spec/services/bulk_create_integration_service_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe BulkCreateIntegrationService do
end
context 'integration with data fields' do
- let(:excluded_attributes) { %w[id service_id created_at updated_at] }
+ let(:excluded_attributes) { %w[id service_id integration_id created_at updated_at] }
it 'updates the data fields from inherited integrations' do
described_class.new(integration, batch, association).execute
@@ -74,7 +74,7 @@ RSpec.describe BulkCreateIntegrationService do
context 'with a project association' do
let!(:project) { create(:project, group: group) }
- let(:integration) { create(:jira_integration, group: group, project: nil) }
+ let(:integration) { create(:jira_integration, :group, group: group) }
let(:created_integration) { project.jira_integration }
let(:batch) { Project.where(id: Project.minimum(:id)..Project.maximum(:id)).without_integration(integration).in_namespace(integration.group.self_and_descendants) }
let(:association) { 'project' }
@@ -82,11 +82,19 @@ RSpec.describe BulkCreateIntegrationService do
it_behaves_like 'creates integration from batch ids'
it_behaves_like 'updates inherit_from_id'
+
+ context 'with different foreign key of data_fields' do
+ let(:integration) { create(:zentao_integration, :group, group: group) }
+ let(:created_integration) { project.zentao_integration }
+
+ it_behaves_like 'creates integration from batch ids'
+ it_behaves_like 'updates inherit_from_id'
+ end
end
context 'with a group association' do
let!(:subgroup) { create(:group, parent: group) }
- let(:integration) { create(:jira_integration, group: group, project: nil, inherit_from_id: instance_integration.id) }
+ let(:integration) { create(:jira_integration, :group, group: group, inherit_from_id: instance_integration.id) }
let(:created_integration) { Integration.find_by(group: subgroup) }
let(:batch) { Group.where(id: subgroup.id) }
let(:association) { 'group' }
@@ -94,6 +102,13 @@ RSpec.describe BulkCreateIntegrationService do
it_behaves_like 'creates integration from batch ids'
it_behaves_like 'updates inherit_from_id'
+
+ context 'with different foreign key of data_fields' do
+ let(:integration) { create(:zentao_integration, :group, group: group, inherit_from_id: instance_integration.id) }
+
+ it_behaves_like 'creates integration from batch ids'
+ it_behaves_like 'updates inherit_from_id'
+ end
end
end
end
diff --git a/spec/services/bulk_update_integration_service_spec.rb b/spec/services/bulk_update_integration_service_spec.rb
index c10a9b75648..5e521b98482 100644
--- a/spec/services/bulk_update_integration_service_spec.rb
+++ b/spec/services/bulk_update_integration_service_spec.rb
@@ -16,32 +16,19 @@ RSpec.describe BulkUpdateIntegrationService do
let_it_be(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group) }
- let_it_be(:group_integration) do
- Integrations::Jira.create!(
- group: group,
- url: 'http://group.jira.com'
- )
- end
-
+ let_it_be(:group_integration) { create(:jira_integration, :group, group: group, url: 'http://group.jira.com') }
+ let_it_be(:excluded_integration) { create(:jira_integration, :group, group: create(:group), url: 'http://another.jira.com', push_events: false) }
let_it_be(:subgroup_integration) do
- Integrations::Jira.create!(
- inherit_from_id: group_integration.id,
+ create(:jira_integration, :group,
group: subgroup,
+ inherit_from_id: group_integration.id,
url: 'http://subgroup.jira.com',
push_events: true
)
end
- let_it_be(:excluded_integration) do
- Integrations::Jira.create!(
- group: create(:group),
- url: 'http://another.jira.com',
- push_events: false
- )
- end
-
let_it_be(:integration) do
- Integrations::Jira.create!(
+ create(:jira_integration,
project: create(:project, group: subgroup),
inherit_from_id: subgroup_integration.id,
url: 'http://project.jira.com',
@@ -88,4 +75,22 @@ RSpec.describe BulkUpdateIntegrationService do
described_class.new(group_integration, [integration]).execute
end.to change { integration.reload.url }.to(group_integration.url)
end
+
+ context 'with different foreign key of data_fields' do
+ let(:integration) { create(:zentao_integration, project: create(:project, group: group)) }
+ let(:group_integration) do
+ create(:zentao_integration, :group,
+ group: group,
+ url: 'https://group.zentao.net',
+ api_token: 'GROUP_TOKEN',
+ zentao_product_xid: '1'
+ )
+ end
+
+ it 'works with batch as an array of ActiveRecord objects' do
+ expect do
+ described_class.new(group_integration, [integration]).execute
+ end.to change { integration.reload.url }.to(group_integration.url)
+ end
+ end
end
diff --git a/spec/services/ci/create_pipeline_service/include_spec.rb b/spec/services/ci/create_pipeline_service/include_spec.rb
index 5e7dace8e15..aa01977272a 100644
--- a/spec/services/ci/create_pipeline_service/include_spec.rb
+++ b/spec/services/ci/create_pipeline_service/include_spec.rb
@@ -7,9 +7,11 @@ RSpec.describe Ci::CreatePipelineService 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(:ref) { 'refs/heads/master' }
+ let(:variables_attributes) { [{ key: 'MYVAR', secret_value: 'hello' }] }
+ let(:source) { :push }
+
+ let(:service) { described_class.new(project, user, { ref: ref, variables_attributes: variables_attributes }) }
let(:pipeline) { service.execute(source).payload }
let(:file_location) { 'spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml' }
@@ -24,6 +26,20 @@ RSpec.describe Ci::CreatePipelineService do
.and_return(File.read(Rails.root.join(file_location)))
end
+ shared_examples 'not including the file' do
+ it 'does not include the job in the file' do
+ expect(pipeline).to be_created_successfully
+ expect(pipeline.processables.pluck(:name)).to contain_exactly('job')
+ end
+ end
+
+ shared_examples 'including the file' do
+ it 'includes the job in the file' do
+ expect(pipeline).to be_created_successfully
+ expect(pipeline.processables.pluck(:name)).to contain_exactly('job', 'rspec')
+ end
+ end
+
context 'with a local file' do
let(:config) do
<<~EOY
@@ -33,13 +49,10 @@ RSpec.describe Ci::CreatePipelineService do
EOY
end
- it 'includes the job in the file' do
- expect(pipeline).to be_created_successfully
- expect(pipeline.processables.pluck(:name)).to contain_exactly('job', 'rspec')
- end
+ it_behaves_like 'including the file'
end
- context 'with a local file with rules' do
+ context 'with a local file with rules with a project variable' do
let(:config) do
<<~EOY
include:
@@ -54,19 +67,63 @@ RSpec.describe Ci::CreatePipelineService do
context 'when the rules matches' do
let(:project_id) { project.id }
- it 'includes the job in the file' do
- expect(pipeline).to be_created_successfully
- expect(pipeline.processables.pluck(:name)).to contain_exactly('job', 'rspec')
- end
+ it_behaves_like 'including the file'
end
context 'when the rules does not match' do
let(:project_id) { non_existing_record_id }
- it 'does not include the job in the file' do
- expect(pipeline).to be_created_successfully
- expect(pipeline.processables.pluck(:name)).to contain_exactly('job')
- end
+ it_behaves_like 'not including the file'
+ end
+ end
+
+ context 'with a local file with rules with a predefined pipeline variable' do
+ let(:config) do
+ <<~EOY
+ include:
+ - local: #{file_location}
+ rules:
+ - if: $CI_PIPELINE_SOURCE == "#{pipeline_source}"
+ job:
+ script: exit 0
+ EOY
+ end
+
+ context 'when the rules matches' do
+ let(:pipeline_source) { 'push' }
+
+ it_behaves_like 'including the file'
+ end
+
+ context 'when the rules does not match' do
+ let(:pipeline_source) { 'web' }
+
+ it_behaves_like 'not including the file'
+ end
+ end
+
+ context 'with a local file with rules with a run pipeline variable' do
+ let(:config) do
+ <<~EOY
+ include:
+ - local: #{file_location}
+ rules:
+ - if: $MYVAR == "#{my_var}"
+ job:
+ script: exit 0
+ EOY
+ end
+
+ context 'when the rules matches' do
+ let(:my_var) { 'hello' }
+
+ it_behaves_like 'including the file'
+ end
+
+ context 'when the rules does not match' do
+ let(:my_var) { 'mello' }
+
+ it_behaves_like 'not including the file'
end
end
end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 78646665539..c78e19ea62d 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -5,8 +5,8 @@ require 'spec_helper'
RSpec.describe Ci::CreatePipelineService do
include ProjectForksHelper
- let_it_be(:project, reload: true) { create(:project, :repository) }
- let_it_be(:user, reload: true) { project.owner }
+ let_it_be_with_refind(:project) { create(:project, :repository) }
+ let_it_be_with_reload(:user) { project.owner }
let(:ref_name) { 'refs/heads/master' }
diff --git a/spec/services/ci/external_pull_requests/create_pipeline_service_spec.rb b/spec/services/ci/external_pull_requests/create_pipeline_service_spec.rb
index 04d75630295..d5881d3b204 100644
--- a/spec/services/ci/external_pull_requests/create_pipeline_service_spec.rb
+++ b/spec/services/ci/external_pull_requests/create_pipeline_service_spec.rb
@@ -26,28 +26,6 @@ RSpec.describe Ci::ExternalPullRequests::CreatePipelineService do
pull_request.update!(source_branch: source_branch.name, source_sha: source_branch.target)
end
- context 'when the FF ci_create_external_pr_pipeline_async is disabled' do
- before do
- stub_feature_flags(ci_create_external_pr_pipeline_async: false)
- end
-
- it 'creates a pipeline for external pull request', :aggregate_failures do
- pipeline = execute.payload
-
- expect(execute).to be_success
- expect(pipeline).to be_valid
- expect(pipeline).to be_persisted
- expect(pipeline).to be_external_pull_request_event
- expect(pipeline).to eq(project.ci_pipelines.last)
- expect(pipeline.external_pull_request).to eq(pull_request)
- expect(pipeline.user).to eq(user)
- expect(pipeline.status).to eq('created')
- expect(pipeline.ref).to eq(pull_request.source_branch)
- expect(pipeline.sha).to eq(pull_request.source_sha)
- expect(pipeline.source_sha).to eq(pull_request.source_sha)
- end
- end
-
it 'enqueues Ci::ExternalPullRequests::CreatePipelineWorker' do
expect { execute }
.to change { ::Ci::ExternalPullRequests::CreatePipelineWorker.jobs.count }
diff --git a/spec/services/ci/generate_kubeconfig_service_spec.rb b/spec/services/ci/generate_kubeconfig_service_spec.rb
new file mode 100644
index 00000000000..b0673d16158
--- /dev/null
+++ b/spec/services/ci/generate_kubeconfig_service_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::GenerateKubeconfigService do
+ describe '#execute' do
+ let(:project) { create(:project) }
+ let(:build) { create(:ci_build, project: project) }
+ let(:agent1) { create(:cluster_agent, project: project) }
+ let(:agent2) { create(:cluster_agent) }
+
+ let(:template) { instance_double(Gitlab::Kubernetes::Kubeconfig::Template) }
+
+ subject { described_class.new(build).execute }
+
+ before do
+ expect(Gitlab::Kubernetes::Kubeconfig::Template).to receive(:new).and_return(template)
+ expect(build.pipeline).to receive(:authorized_cluster_agents).and_return([agent1, agent2])
+ end
+
+ it 'adds a cluster, and a user and context for each available agent' do
+ expect(template).to receive(:add_cluster).with(
+ name: 'gitlab',
+ url: Gitlab::Kas.tunnel_url
+ ).once
+
+ expect(template).to receive(:add_user).with(
+ name: "agent:#{agent1.id}",
+ token: "ci:#{agent1.id}:#{build.token}"
+ )
+ expect(template).to receive(:add_user).with(
+ name: "agent:#{agent2.id}",
+ token: "ci:#{agent2.id}:#{build.token}"
+ )
+
+ expect(template).to receive(:add_context).with(
+ name: "#{project.full_path}:#{agent1.name}",
+ cluster: 'gitlab',
+ user: "agent:#{agent1.id}"
+ )
+ expect(template).to receive(:add_context).with(
+ name: "#{agent2.project.full_path}:#{agent2.name}",
+ cluster: 'gitlab',
+ user: "agent:#{agent2.id}"
+ )
+
+ expect(subject).to eq(template)
+ end
+ end
+end
diff --git a/spec/services/ci/job_artifacts/create_service_spec.rb b/spec/services/ci/job_artifacts/create_service_spec.rb
index e6d9f208096..6ad3e9ceb54 100644
--- a/spec/services/ci/job_artifacts/create_service_spec.rb
+++ b/spec/services/ci/job_artifacts/create_service_spec.rb
@@ -49,6 +49,7 @@ RSpec.describe Ci::JobArtifacts::CreateService do
expect(new_artifact.file_type).to eq(params['artifact_type'])
expect(new_artifact.file_format).to eq(params['artifact_format'])
expect(new_artifact.file_sha256).to eq(artifacts_sha256)
+ expect(new_artifact.locked).to eq(job.pipeline.locked)
end
it 'does not track the job user_id' do
@@ -75,6 +76,7 @@ RSpec.describe Ci::JobArtifacts::CreateService do
expect(new_artifact.file_type).to eq('metadata')
expect(new_artifact.file_format).to eq('gzip')
expect(new_artifact.file_sha256).to eq(artifacts_sha256)
+ expect(new_artifact.locked).to eq(job.pipeline.locked)
end
it 'sets expiration date according to application settings' do
@@ -175,18 +177,6 @@ RSpec.describe Ci::JobArtifacts::CreateService do
hash_including('key' => 'KEY1', 'value' => 'VAR1', 'source' => 'dotenv'),
hash_including('key' => 'KEY2', 'value' => 'VAR2', 'source' => 'dotenv'))
end
-
- context 'when ci_synchronous_artifact_parsing feature flag is disabled' do
- before do
- stub_feature_flags(ci_synchronous_artifact_parsing: false)
- end
-
- it 'does not call parse service' do
- expect(Ci::ParseDotenvArtifactService).not_to receive(:new)
-
- expect(subject[:status]).to eq(:success)
- end
- end
end
context 'when artifact_type is metrics' do
diff --git a/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb b/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb
index 7a91ad9dcc1..6761f052e18 100644
--- a/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb
+++ b/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb
@@ -16,26 +16,43 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s
let_it_be(:job) { create(:ci_build, :success, pipeline: pipeline) }
context 'when artifact is expired' do
- let!(:artifact) { create(:ci_job_artifact, :expired, job: job) }
+ let!(:artifact) { create(:ci_job_artifact, :expired, job: job, locked: job.pipeline.locked) }
context 'with preloaded relationships' do
before do
stub_const("#{described_class}::LOOP_LIMIT", 1)
end
- it 'performs the smallest number of queries for job_artifacts' do
- log = ActiveRecord::QueryRecorder.new { subject }
+ context 'with ci_destroy_unlocked_job_artifacts feature flag disabled' do
+ before do
+ stub_feature_flags(ci_destroy_unlocked_job_artifacts: false)
+ end
+
+ it 'performs the smallest number of queries for job_artifacts' do
+ log = ActiveRecord::QueryRecorder.new { subject }
+
+ # SELECT expired ci_job_artifacts - 3 queries from each_batch
+ # PRELOAD projects, routes, project_statistics
+ # BEGIN
+ # INSERT into ci_deleted_objects
+ # DELETE loaded ci_job_artifacts
+ # DELETE security_findings -- for EE
+ # COMMIT
+ # SELECT next expired ci_job_artifacts
+
+ expect(log.count).to be_within(1).of(10)
+ end
+ end
- # SELECT expired ci_job_artifacts - 3 queries from each_batch
- # PRELOAD projects, routes, project_statistics
- # BEGIN
- # INSERT into ci_deleted_objects
- # DELETE loaded ci_job_artifacts
- # DELETE security_findings -- for EE
- # COMMIT
- # SELECT next expired ci_job_artifacts
+ context 'with ci_destroy_unlocked_job_artifacts feature flag enabled' do
+ before do
+ stub_feature_flags(ci_destroy_unlocked_job_artifacts: true)
+ end
- expect(log.count).to be_within(1).of(10)
+ it 'performs the smallest number of queries for job_artifacts' do
+ log = ActiveRecord::QueryRecorder.new { subject }
+ expect(log.count).to be_within(1).of(8)
+ end
end
end
@@ -53,7 +70,7 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s
end
context 'when the artifact has a file attached to it' do
- let!(:artifact) { create(:ci_job_artifact, :expired, :zip, job: job) }
+ let!(:artifact) { create(:ci_job_artifact, :expired, :zip, job: job, locked: job.pipeline.locked) }
it 'creates a deleted object' do
expect { subject }.to change { Ci::DeletedObject.count }.by(1)
@@ -74,7 +91,7 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s
end
context 'when artifact is locked' do
- let!(:artifact) { create(:ci_job_artifact, :expired, job: locked_job) }
+ let!(:artifact) { create(:ci_job_artifact, :expired, job: locked_job, locked: locked_job.pipeline.locked) }
it 'does not destroy job artifact' do
expect { subject }.not_to change { Ci::JobArtifact.count }
@@ -83,7 +100,7 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s
end
context 'when artifact is not expired' do
- let!(:artifact) { create(:ci_job_artifact, job: job) }
+ let!(:artifact) { create(:ci_job_artifact, job: job, locked: job.pipeline.locked) }
it 'does not destroy expired job artifacts' do
expect { subject }.not_to change { Ci::JobArtifact.count }
@@ -91,7 +108,7 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s
end
context 'when artifact is permanent' do
- let!(:artifact) { create(:ci_job_artifact, expire_at: nil, job: job) }
+ let!(:artifact) { create(:ci_job_artifact, expire_at: nil, job: job, locked: job.pipeline.locked) }
it 'does not destroy expired job artifacts' do
expect { subject }.not_to change { Ci::JobArtifact.count }
@@ -99,7 +116,7 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s
end
context 'when failed to destroy artifact' do
- let!(:artifact) { create(:ci_job_artifact, :expired, job: job) }
+ let!(:artifact) { create(:ci_job_artifact, :expired, job: job, locked: job.pipeline.locked) }
before do
stub_const("#{described_class}::LOOP_LIMIT", 10)
@@ -135,7 +152,7 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s
end
context 'when exclusive lease has already been taken by the other instance' do
- let!(:artifact) { create(:ci_job_artifact, :expired, job: job) }
+ let!(:artifact) { create(:ci_job_artifact, :expired, job: job, locked: job.pipeline.locked) }
before do
stub_exclusive_lease_taken(described_class::EXCLUSIVE_LOCK_KEY, timeout: described_class::LOCK_TIMEOUT)
@@ -149,8 +166,8 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s
context 'with a second artifact and batch size of 1' do
let(:second_job) { create(:ci_build, :success, pipeline: pipeline) }
- let!(:second_artifact) { create(:ci_job_artifact, :archive, expire_at: 1.day.ago, job: second_job) }
- let!(:artifact) { create(:ci_job_artifact, :expired, job: job) }
+ let!(:second_artifact) { create(:ci_job_artifact, :archive, expire_at: 1.day.ago, job: second_job, locked: job.pipeline.locked) }
+ let!(:artifact) { create(:ci_job_artifact, :expired, job: job, locked: job.pipeline.locked) }
before do
stub_const("#{described_class}::BATCH_SIZE", 1)
@@ -206,8 +223,8 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s
end
context 'when some artifacts are locked' do
- let!(:artifact) { create(:ci_job_artifact, :expired, job: job) }
- let!(:locked_artifact) { create(:ci_job_artifact, :expired, job: locked_job) }
+ let!(:artifact) { create(:ci_job_artifact, :expired, job: job, locked: job.pipeline.locked) }
+ let!(:locked_artifact) { create(:ci_job_artifact, :expired, job: locked_job, locked: locked_job.pipeline.locked) }
it 'destroys only unlocked artifacts' do
expect { subject }.to change { Ci::JobArtifact.count }.by(-1)
@@ -216,7 +233,7 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s
end
context 'when all artifacts are locked' do
- let!(:artifact) { create(:ci_job_artifact, :expired, job: locked_job) }
+ let!(:artifact) { create(:ci_job_artifact, :expired, job: locked_job, locked: locked_job.pipeline.locked) }
it 'destroys no artifacts' do
expect { subject }.to change { Ci::JobArtifact.count }.by(0)
diff --git a/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb b/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb
index 2cedbf93d74..1cc856734fc 100644
--- a/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb
+++ b/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb
@@ -29,7 +29,8 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do
it 'reports metrics for destroyed artifacts' do
expect_next_instance_of(Gitlab::Ci::Artifacts::Metrics) do |metrics|
- expect(metrics).to receive(:increment_destroyed_artifacts).with(1).and_call_original
+ expect(metrics).to receive(:increment_destroyed_artifacts_count).with(1).and_call_original
+ expect(metrics).to receive(:increment_destroyed_artifacts_bytes).with(107464).and_call_original
end
execute
diff --git a/spec/services/ci/parse_dotenv_artifact_service_spec.rb b/spec/services/ci/parse_dotenv_artifact_service_spec.rb
index 7536e04f2de..c4040a426f2 100644
--- a/spec/services/ci/parse_dotenv_artifact_service_spec.rb
+++ b/spec/services/ci/parse_dotenv_artifact_service_spec.rb
@@ -45,7 +45,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do
it 'returns error' do
expect(subject[:status]).to eq(:error)
- expect(subject[:message]).to eq("Dotenv Artifact Too Big. Maximum Allowable Size: #{described_class::MAX_ACCEPTABLE_DOTENV_SIZE}")
+ expect(subject[:message]).to eq("Dotenv Artifact Too Big. Maximum Allowable Size: #{service.send(:dotenv_size_limit)}")
expect(subject[:http_status]).to eq(:bad_request)
end
end
@@ -186,7 +186,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do
context 'when more than limitated variables are specified in dotenv' do
let(:blob) do
StringIO.new.tap do |s|
- (described_class::MAX_ACCEPTABLE_VARIABLES_COUNT + 1).times do |i|
+ (service.send(:dotenv_variable_limit) + 1).times do |i|
s << "KEY#{i}=VAR#{i}\n"
end
end.string
@@ -194,7 +194,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do
it 'returns error' do
expect(subject[:status]).to eq(:error)
- expect(subject[:message]).to eq("Dotenv files cannot have more than #{described_class::MAX_ACCEPTABLE_VARIABLES_COUNT} variables")
+ expect(subject[:message]).to eq("Dotenv files cannot have more than #{service.send(:dotenv_variable_limit)} variables")
expect(subject[:http_status]).to eq(:bad_request)
end
end
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index 15c88c9f657..16635c64434 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -323,6 +323,37 @@ RSpec.describe Ci::RetryBuildService do
it 'persists expanded environment name' do
expect(new_build.metadata.expanded_environment_name).to eq('production')
end
+
+ it 'does not create a new environment' do
+ expect { new_build }.not_to change { Environment.count }
+ end
+ end
+
+ context 'when build with dynamic environment is retried' do
+ let_it_be(:other_developer) { create(:user).tap { |u| project.add_developer(other_developer) } }
+
+ let(:environment_name) { 'review/$CI_COMMIT_REF_SLUG-$GITLAB_USER_ID' }
+
+ let!(:build) do
+ create(:ci_build, :with_deployment, environment: environment_name,
+ options: { environment: { name: environment_name } },
+ pipeline: pipeline, stage_id: stage.id, project: project,
+ user: other_developer)
+ end
+
+ it 're-uses the previous persisted environment' do
+ expect(build.persisted_environment.name).to eq("review/#{build.ref}-#{other_developer.id}")
+
+ expect(new_build.persisted_environment.name).to eq("review/#{build.ref}-#{other_developer.id}")
+ end
+
+ it 'creates a new deployment' do
+ expect { new_build }.to change { Deployment.count }.by(1)
+ end
+
+ it 'does not create a new environment' do
+ expect { new_build }.not_to change { Environment.count }
+ end
end
context 'when build has needs' do
diff --git a/spec/services/ci/unlock_artifacts_service_spec.rb b/spec/services/ci/unlock_artifacts_service_spec.rb
index 8d289a867ba..8ee07fc44c8 100644
--- a/spec/services/ci/unlock_artifacts_service_spec.rb
+++ b/spec/services/ci/unlock_artifacts_service_spec.rb
@@ -3,93 +3,247 @@
require 'spec_helper'
RSpec.describe Ci::UnlockArtifactsService do
- describe '#execute' do
- subject(:execute) { described_class.new(pipeline.project, pipeline.user).execute(ci_ref, before_pipeline) }
+ using RSpec::Parameterized::TableSyntax
+
+ where(:tag, :ci_update_unlocked_job_artifacts) do
+ false | false
+ false | true
+ true | false
+ true | true
+ end
+
+ with_them do
+ let(:ref) { 'master' }
+ let(:ref_path) { tag ? "#{::Gitlab::Git::TAG_REF_PREFIX}#{ref}" : "#{::Gitlab::Git::BRANCH_REF_PREFIX}#{ref}" }
+ let(:ci_ref) { create(:ci_ref, ref_path: ref_path) }
+ let(:project) { ci_ref.project }
+ let(:source_job) { create(:ci_build, pipeline: pipeline) }
+
+ let!(:old_unlocked_pipeline) { create(:ci_pipeline, :with_persisted_artifacts, ref: ref, tag: tag, project: project, locked: :unlocked) }
+ let!(:older_pipeline) { create(:ci_pipeline, :with_persisted_artifacts, ref: ref, tag: tag, project: project, locked: :artifacts_locked) }
+ let!(:older_ambiguous_pipeline) { create(:ci_pipeline, :with_persisted_artifacts, ref: ref, tag: !tag, project: project, locked: :artifacts_locked) }
+ let!(:pipeline) { create(:ci_pipeline, :with_persisted_artifacts, ref: ref, tag: tag, project: project, locked: :artifacts_locked) }
+ let!(:child_pipeline) { create(:ci_pipeline, :with_persisted_artifacts, ref: ref, tag: tag, project: project, locked: :artifacts_locked) }
+ let!(:newer_pipeline) { create(:ci_pipeline, :with_persisted_artifacts, ref: ref, tag: tag, project: project, locked: :artifacts_locked) }
+ let!(:other_ref_pipeline) { create(:ci_pipeline, :with_persisted_artifacts, ref: 'other_ref', tag: tag, project: project, locked: :artifacts_locked) }
+ let!(:sources_pipeline) { create(:ci_sources_pipeline, source_job: source_job, source_project: project, pipeline: child_pipeline, project: project) }
before do
stub_const("#{described_class}::BATCH_SIZE", 1)
+ stub_feature_flags(ci_update_unlocked_job_artifacts: ci_update_unlocked_job_artifacts)
end
- [true, false].each do |tag|
- context "when tag is #{tag}" do
- let(:ref) { 'master' }
- let(:ref_path) { tag ? "#{::Gitlab::Git::TAG_REF_PREFIX}#{ref}" : "#{::Gitlab::Git::BRANCH_REF_PREFIX}#{ref}" }
- let(:ci_ref) { create(:ci_ref, ref_path: ref_path) }
+ describe '#execute' do
+ subject(:execute) { described_class.new(pipeline.project, pipeline.user).execute(ci_ref, before_pipeline) }
+
+ context 'when running on a ref before a pipeline' do
+ let(:before_pipeline) { pipeline }
+
+ it 'unlocks artifacts from older pipelines' do
+ expect { execute }.to change { older_pipeline.reload.locked }.from('artifacts_locked').to('unlocked')
+ end
+
+ it 'does not unlock artifacts for tag or branch with same name as ref' do
+ expect { execute }.not_to change { older_ambiguous_pipeline.reload.locked }.from('artifacts_locked')
+ end
+
+ it 'does not unlock artifacts from newer pipelines' do
+ expect { execute }.not_to change { newer_pipeline.reload.locked }.from('artifacts_locked')
+ end
+
+ it 'does not lock artifacts from old unlocked pipelines' do
+ expect { execute }.not_to change { old_unlocked_pipeline.reload.locked }.from('unlocked')
+ end
+
+ it 'does not unlock artifacts from the same pipeline' do
+ expect { execute }.not_to change { pipeline.reload.locked }.from('artifacts_locked')
+ end
- let!(:old_unlocked_pipeline) { create(:ci_pipeline, ref: ref, tag: tag, project: ci_ref.project, locked: :unlocked) }
- let!(:older_pipeline) { create(:ci_pipeline, ref: ref, tag: tag, project: ci_ref.project, locked: :artifacts_locked) }
- let!(:older_ambiguous_pipeline) { create(:ci_pipeline, ref: ref, tag: !tag, project: ci_ref.project, locked: :artifacts_locked) }
- let!(:pipeline) { create(:ci_pipeline, ref: ref, tag: tag, project: ci_ref.project, locked: :artifacts_locked) }
- let!(:child_pipeline) { create(:ci_pipeline, ref: ref, tag: tag, project: ci_ref.project, locked: :artifacts_locked) }
- let!(:newer_pipeline) { create(:ci_pipeline, ref: ref, tag: tag, project: ci_ref.project, locked: :artifacts_locked) }
- let!(:other_ref_pipeline) { create(:ci_pipeline, ref: 'other_ref', tag: tag, project: ci_ref.project, locked: :artifacts_locked) }
+ it 'does not unlock artifacts for other refs' do
+ expect { execute }.not_to change { other_ref_pipeline.reload.locked }.from('artifacts_locked')
+ end
- before do
- create(:ci_sources_pipeline,
- source_job: create(:ci_build, pipeline: pipeline),
- source_project: ci_ref.project,
- pipeline: child_pipeline,
- project: ci_ref.project)
+ it 'does not unlock artifacts for child pipeline' do
+ expect { execute }.not_to change { child_pipeline.reload.locked }.from('artifacts_locked')
end
- context 'when running on a ref before a pipeline' do
- let(:before_pipeline) { pipeline }
+ it 'unlocks job artifact records' do
+ pending unless ci_update_unlocked_job_artifacts
- it 'unlocks artifacts from older pipelines' do
- expect { execute }.to change { older_pipeline.reload.locked }.from('artifacts_locked').to('unlocked')
- end
+ expect { execute }.to change { ::Ci::JobArtifact.artifact_unlocked.count }.from(0).to(2)
+ end
+ end
- it 'does not unlock artifacts for tag or branch with same name as ref' do
- expect { execute }.not_to change { older_ambiguous_pipeline.reload.locked }.from('artifacts_locked')
- end
+ context 'when running on just the ref' do
+ let(:before_pipeline) { nil }
- it 'does not unlock artifacts from newer pipelines' do
- expect { execute }.not_to change { newer_pipeline.reload.locked }.from('artifacts_locked')
- end
+ it 'unlocks artifacts from older pipelines' do
+ expect { execute }.to change { older_pipeline.reload.locked }.from('artifacts_locked').to('unlocked')
+ end
- it 'does not lock artifacts from old unlocked pipelines' do
- expect { execute }.not_to change { old_unlocked_pipeline.reload.locked }.from('unlocked')
- end
+ it 'unlocks artifacts from newer pipelines' do
+ expect { execute }.to change { newer_pipeline.reload.locked }.from('artifacts_locked').to('unlocked')
+ end
- it 'does not unlock artifacts from the same pipeline' do
- expect { execute }.not_to change { pipeline.reload.locked }.from('artifacts_locked')
- end
+ it 'unlocks artifacts from the same pipeline' do
+ expect { execute }.to change { pipeline.reload.locked }.from('artifacts_locked').to('unlocked')
+ end
- it 'does not unlock artifacts for other refs' do
- expect { execute }.not_to change { other_ref_pipeline.reload.locked }.from('artifacts_locked')
- end
+ it 'does not unlock artifacts for tag or branch with same name as ref' do
+ expect { execute }.not_to change { older_ambiguous_pipeline.reload.locked }.from('artifacts_locked')
+ end
- it 'does not unlock artifacts for child pipeline' do
- expect { execute }.not_to change { child_pipeline.reload.locked }.from('artifacts_locked')
- end
+ it 'does not lock artifacts from old unlocked pipelines' do
+ expect { execute }.not_to change { old_unlocked_pipeline.reload.locked }.from('unlocked')
end
- context 'when running on just the ref' do
- let(:before_pipeline) { nil }
+ it 'does not unlock artifacts for other refs' do
+ expect { execute }.not_to change { other_ref_pipeline.reload.locked }.from('artifacts_locked')
+ end
- it 'unlocks artifacts from older pipelines' do
- expect { execute }.to change { older_pipeline.reload.locked }.from('artifacts_locked').to('unlocked')
- end
+ it 'unlocks job artifact records' do
+ pending unless ci_update_unlocked_job_artifacts
- it 'unlocks artifacts from newer pipelines' do
- expect { execute }.to change { newer_pipeline.reload.locked }.from('artifacts_locked').to('unlocked')
- end
+ expect { execute }.to change { ::Ci::JobArtifact.artifact_unlocked.count }.from(0).to(8)
+ end
+ end
+ end
- it 'unlocks artifacts from the same pipeline' do
- expect { execute }.to change { pipeline.reload.locked }.from('artifacts_locked').to('unlocked')
- end
+ describe '#unlock_pipelines_query' do
+ subject { described_class.new(pipeline.project, pipeline.user).unlock_pipelines_query(ci_ref, before_pipeline) }
+
+ context 'when running on a ref before a pipeline' do
+ let(:before_pipeline) { pipeline }
+
+ it 'produces the expected SQL string' do
+ expect(subject.squish).to eq <<~SQL.squish
+ UPDATE
+ "ci_pipelines"
+ SET
+ "locked" = 0
+ WHERE
+ "ci_pipelines"."id" IN
+ (SELECT
+ "ci_pipelines"."id"
+ FROM
+ "ci_pipelines"
+ WHERE
+ "ci_pipelines"."ci_ref_id" = #{ci_ref.id}
+ AND "ci_pipelines"."locked" = 1
+ AND (ci_pipelines.id < #{before_pipeline.id})
+ AND "ci_pipelines"."id" NOT IN
+ (WITH RECURSIVE
+ "base_and_descendants"
+ AS
+ ((SELECT
+ "ci_pipelines".*
+ FROM
+ "ci_pipelines"
+ WHERE
+ "ci_pipelines"."id" = #{before_pipeline.id})
+ UNION
+ (SELECT
+ "ci_pipelines".*
+ FROM
+ "ci_pipelines",
+ "base_and_descendants",
+ "ci_sources_pipelines"
+ WHERE
+ "ci_sources_pipelines"."pipeline_id" = "ci_pipelines"."id"
+ AND "ci_sources_pipelines"."source_pipeline_id" = "base_and_descendants"."id"
+ AND "ci_sources_pipelines"."source_project_id" = "ci_sources_pipelines"."project_id"))
+ SELECT
+ "id"
+ FROM
+ "base_and_descendants"
+ AS
+ "ci_pipelines")
+ LIMIT 1
+ FOR UPDATE
+ SKIP LOCKED)
+ RETURNING ("ci_pipelines"."id")
+ SQL
+ end
+ end
- it 'does not unlock artifacts for tag or branch with same name as ref' do
- expect { execute }.not_to change { older_ambiguous_pipeline.reload.locked }.from('artifacts_locked')
- end
+ context 'when running on just the ref' do
+ let(:before_pipeline) { nil }
+
+ it 'produces the expected SQL string' do
+ expect(subject.squish).to eq <<~SQL.squish
+ UPDATE
+ "ci_pipelines"
+ SET
+ "locked" = 0
+ WHERE
+ "ci_pipelines"."id" IN
+ (SELECT
+ "ci_pipelines"."id"
+ FROM
+ "ci_pipelines"
+ WHERE
+ "ci_pipelines"."ci_ref_id" = #{ci_ref.id}
+ AND "ci_pipelines"."locked" = 1
+ LIMIT 1
+ FOR UPDATE
+ SKIP LOCKED)
+ RETURNING
+ ("ci_pipelines"."id")
+ SQL
+ end
+ end
+ end
- it 'does not lock artifacts from old unlocked pipelines' do
- expect { execute }.not_to change { old_unlocked_pipeline.reload.locked }.from('unlocked')
- end
+ describe '#unlock_job_artifacts_query' do
+ subject { described_class.new(pipeline.project, pipeline.user).unlock_job_artifacts_query(pipeline_ids) }
+
+ context 'when running on a ref before a pipeline' do
+ let(:before_pipeline) { pipeline }
+ let(:pipeline_ids) { [older_pipeline.id] }
+
+ it 'produces the expected SQL string' do
+ expect(subject.squish).to eq <<~SQL.squish
+ UPDATE
+ "ci_job_artifacts"
+ SET
+ "locked" = 0
+ WHERE
+ "ci_job_artifacts"."job_id" IN
+ (SELECT
+ "ci_builds"."id"
+ FROM
+ "ci_builds"
+ WHERE
+ "ci_builds"."type" = 'Ci::Build'
+ AND "ci_builds"."commit_id" = #{older_pipeline.id})
+ RETURNING
+ ("ci_job_artifacts"."id")
+ SQL
+ end
+ end
- it 'does not unlock artifacts for other refs' do
- expect { execute }.not_to change { other_ref_pipeline.reload.locked }.from('artifacts_locked')
- end
+ context 'when running on just the ref' do
+ let(:before_pipeline) { nil }
+ let(:pipeline_ids) { [older_pipeline.id, newer_pipeline.id, pipeline.id] }
+
+ it 'produces the expected SQL string' do
+ expect(subject.squish).to eq <<~SQL.squish
+ UPDATE
+ "ci_job_artifacts"
+ SET
+ "locked" = 0
+ WHERE
+ "ci_job_artifacts"."job_id" IN
+ (SELECT
+ "ci_builds"."id"
+ FROM
+ "ci_builds"
+ WHERE
+ "ci_builds"."type" = 'Ci::Build'
+ AND "ci_builds"."commit_id" IN (#{pipeline_ids.join(', ')}))
+ RETURNING
+ ("ci_job_artifacts"."id")
+ SQL
end
end
end
diff --git a/spec/services/ci/update_build_state_service_spec.rb b/spec/services/ci/update_build_state_service_spec.rb
index e4dd3d0500f..937b19beff5 100644
--- a/spec/services/ci/update_build_state_service_spec.rb
+++ b/spec/services/ci/update_build_state_service_spec.rb
@@ -118,7 +118,7 @@ RSpec.describe Ci::UpdateBuildStateService do
expect(metrics)
.not_to have_received(:increment_error_counter)
- .with(type: :chunks_invalid_checksum)
+ .with(error_reason: :chunks_invalid_checksum)
end
end
@@ -188,7 +188,7 @@ RSpec.describe Ci::UpdateBuildStateService do
expect(metrics)
.to have_received(:increment_error_counter)
- .with(type: :chunks_invalid_checksum)
+ .with(error_reason: :chunks_invalid_checksum)
end
end
@@ -210,11 +210,11 @@ RSpec.describe Ci::UpdateBuildStateService do
expect(metrics)
.not_to have_received(:increment_error_counter)
- .with(type: :chunks_invalid_checksum)
+ .with(error_reason: :chunks_invalid_checksum)
expect(metrics)
.not_to have_received(:increment_error_counter)
- .with(type: :chunks_invalid_size)
+ .with(error_reason: :chunks_invalid_size)
end
context 'when using deprecated parameters' do
@@ -235,11 +235,11 @@ RSpec.describe Ci::UpdateBuildStateService do
expect(metrics)
.not_to have_received(:increment_error_counter)
- .with(type: :chunks_invalid_checksum)
+ .with(error_reason: :chunks_invalid_checksum)
expect(metrics)
.not_to have_received(:increment_error_counter)
- .with(type: :chunks_invalid_size)
+ .with(error_reason: :chunks_invalid_size)
end
end
end
@@ -262,11 +262,11 @@ RSpec.describe Ci::UpdateBuildStateService do
expect(metrics)
.to have_received(:increment_error_counter)
- .with(type: :chunks_invalid_checksum)
+ .with(error_reason: :chunks_invalid_checksum)
expect(metrics)
.to have_received(:increment_error_counter)
- .with(type: :chunks_invalid_size)
+ .with(error_reason: :chunks_invalid_size)
end
end
@@ -284,7 +284,7 @@ RSpec.describe Ci::UpdateBuildStateService do
expect(metrics)
.to have_received(:increment_error_counter)
- .with(type: :chunks_invalid_checksum)
+ .with(error_reason: :chunks_invalid_checksum)
expect(metrics)
.not_to have_received(:increment_trace_operation)
@@ -292,7 +292,7 @@ RSpec.describe Ci::UpdateBuildStateService do
expect(metrics)
.not_to have_received(:increment_error_counter)
- .with(type: :chunks_invalid_size)
+ .with(error_reason: :chunks_invalid_size)
end
end
@@ -376,7 +376,7 @@ RSpec.describe Ci::UpdateBuildStateService do
expect(metrics)
.not_to have_received(:increment_error_counter)
- .with(type: :chunks_invalid_checksum)
+ .with(error_reason: :chunks_invalid_checksum)
end
context 'when build pending state is outdated' do
diff --git a/spec/services/clusters/agents/refresh_authorization_service_spec.rb b/spec/services/clusters/agents/refresh_authorization_service_spec.rb
index 77ba81ea9c0..09bec7ae0e8 100644
--- a/spec/services/clusters/agents/refresh_authorization_service_spec.rb
+++ b/spec/services/clusters/agents/refresh_authorization_service_spec.rb
@@ -113,6 +113,16 @@ RSpec.describe Clusters::Agents::RefreshAuthorizationService do
expect(modified_authorization.config).to eq({ 'default_namespace' => 'new-namespace' })
end
+ context 'project does not belong to a group, and is authorizing itself' do
+ let(:root_ancestor) { create(:namespace) }
+ let(:added_project) { project }
+
+ it 'creates an authorization record for the project' do
+ expect(subject).to be_truthy
+ expect(agent.authorized_projects).to contain_exactly(added_project)
+ end
+ end
+
context 'config contains too many projects' do
before do
stub_const("#{described_class}::AUTHORIZED_ENTITY_LIMIT", 1)
diff --git a/spec/services/clusters/cleanup/project_namespace_service_spec.rb b/spec/services/clusters/cleanup/project_namespace_service_spec.rb
index 605aaea17e4..ec510b2e3c5 100644
--- a/spec/services/clusters/cleanup/project_namespace_service_spec.rb
+++ b/spec/services/clusters/cleanup/project_namespace_service_spec.rb
@@ -58,6 +58,19 @@ RSpec.describe Clusters::Cleanup::ProjectNamespaceService do
subject
end
+
+ context 'when cluster.kubeclient is nil' do
+ let(:kubeclient_instance_double) { nil }
+
+ it 'schedules ::ServiceAccountWorker' do
+ expect(Clusters::Cleanup::ServiceAccountWorker).to receive(:perform_async).with(cluster.id)
+ subject
+ end
+
+ it 'deletes namespaces from database' do
+ expect { subject }.to change { cluster.kubernetes_namespaces.exists? }.from(true).to(false)
+ end
+ end
end
context 'when cluster has no namespaces' do
diff --git a/spec/services/clusters/cleanup/service_account_service_spec.rb b/spec/services/clusters/cleanup/service_account_service_spec.rb
index f256df1b2fc..adcdbd84da0 100644
--- a/spec/services/clusters/cleanup/service_account_service_spec.rb
+++ b/spec/services/clusters/cleanup/service_account_service_spec.rb
@@ -44,5 +44,13 @@ RSpec.describe Clusters::Cleanup::ServiceAccountService do
it 'deletes cluster' do
expect { subject }.to change { Clusters::Cluster.where(id: cluster.id).exists? }.from(true).to(false)
end
+
+ context 'when cluster.kubeclient is nil' do
+ let(:kubeclient_instance_double) { nil }
+
+ it 'deletes cluster' do
+ expect { subject }.to change { Clusters::Cluster.where(id: cluster.id).exists? }.from(true).to(false)
+ end
+ end
end
end
diff --git a/spec/services/clusters/applications/prometheus_health_check_service_spec.rb b/spec/services/clusters/integrations/prometheus_health_check_service_spec.rb
index e6c7b147ab7..9db3b9d2417 100644
--- a/spec/services/clusters/applications/prometheus_health_check_service_spec.rb
+++ b/spec/services/clusters/integrations/prometheus_health_check_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Clusters::Applications::PrometheusHealthCheckService, '#execute' do
+RSpec.describe Clusters::Integrations::PrometheusHealthCheckService, '#execute' do
let(:service) { described_class.new(cluster) }
subject { service.execute }
@@ -26,10 +26,10 @@ RSpec.describe Clusters::Applications::PrometheusHealthCheckService, '#execute'
end
RSpec.shared_examples 'correct health stored' do
- it 'stores the correct health of prometheus app' do
+ it 'stores the correct health of prometheus' do
subject
- expect(prometheus.healthy).to eq(client_healthy)
+ expect(prometheus.healthy?).to eq(client_healthy)
end
end
@@ -43,19 +43,19 @@ RSpec.describe Clusters::Applications::PrometheusHealthCheckService, '#execute'
let_it_be(:project) { create(:project) }
let_it_be(:integration) { create(:alert_management_http_integration, project: project) }
- let(:applications_prometheus_healthy) { true }
- let(:prometheus) { create(:clusters_applications_prometheus, status: prometheus_status_value, healthy: applications_prometheus_healthy) }
- let(:cluster) { create(:cluster, :project, application_prometheus: prometheus, projects: [project]) }
+ let(:previous_health_status) { :healthy }
+ let(:prometheus) { create(:clusters_integrations_prometheus, enabled: prometheus_enabled, health_status: previous_health_status) }
+ let(:cluster) { create(:cluster, :project, integration_prometheus: prometheus, projects: [project]) }
- context 'when prometheus not installed' do
- let(:prometheus_status_value) { Clusters::Applications::Prometheus.state_machine.states[:installing].value }
+ context 'when prometheus not enabled' do
+ let(:prometheus_enabled) { false }
it { expect(subject).to eq(nil) }
include_examples 'no alert'
end
- context 'when prometheus installed' do
- let(:prometheus_status_value) { Clusters::Applications::Prometheus.state_machine.states[:installed].value }
+ context 'when prometheus enabled' do
+ let(:prometheus_enabled) { true }
before do
client = instance_double('PrometheusClient', healthy?: client_healthy)
@@ -63,7 +63,7 @@ RSpec.describe Clusters::Applications::PrometheusHealthCheckService, '#execute'
end
context 'when newly unhealthy' do
- let(:applications_prometheus_healthy) { true }
+ let(:previous_health_status) { :healthy }
let(:client_healthy) { false }
include_examples 'sends alert'
@@ -71,7 +71,7 @@ RSpec.describe Clusters::Applications::PrometheusHealthCheckService, '#execute'
end
context 'when newly healthy' do
- let(:applications_prometheus_healthy) { false }
+ let(:previous_health_status) { :unhealthy }
let(:client_healthy) { true }
include_examples 'no alert'
@@ -79,7 +79,7 @@ RSpec.describe Clusters::Applications::PrometheusHealthCheckService, '#execute'
end
context 'when continuously unhealthy' do
- let(:applications_prometheus_healthy) { false }
+ let(:previous_health_status) { :unhealthy }
let(:client_healthy) { false }
include_examples 'no alert'
@@ -87,7 +87,7 @@ RSpec.describe Clusters::Applications::PrometheusHealthCheckService, '#execute'
end
context 'when continuously healthy' do
- let(:applications_prometheus_healthy) { true }
+ let(:previous_health_status) { :healthy }
let(:client_healthy) { true }
include_examples 'no alert'
@@ -95,7 +95,7 @@ RSpec.describe Clusters::Applications::PrometheusHealthCheckService, '#execute'
end
context 'when first health check and healthy' do
- let(:applications_prometheus_healthy) { nil }
+ let(:previous_health_status) { :unknown }
let(:client_healthy) { true }
include_examples 'no alert'
@@ -103,7 +103,7 @@ RSpec.describe Clusters::Applications::PrometheusHealthCheckService, '#execute'
end
context 'when first health check and not healthy' do
- let(:applications_prometheus_healthy) { nil }
+ let(:previous_health_status) { :unknown }
let(:client_healthy) { false }
include_examples 'sends alert'
diff --git a/spec/services/dependency_proxy/find_or_create_blob_service_spec.rb b/spec/services/dependency_proxy/find_or_create_blob_service_spec.rb
index 20b0546effa..5f7afdf699a 100644
--- a/spec/services/dependency_proxy/find_or_create_blob_service_spec.rb
+++ b/spec/services/dependency_proxy/find_or_create_blob_service_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe DependencyProxy::FindOrCreateBlobService do
let(:blob_sha) { blob.file_name.sub('.gz', '') }
it 'uses cached blob instead of downloading one' do
- expect { subject }.to change { blob.reload.updated_at }
+ expect { subject }.to change { blob.reload.read_at }
expect(subject[:status]).to eq(:success)
expect(subject[:blob]).to be_a(DependencyProxy::Blob)
diff --git a/spec/services/dependency_proxy/find_or_create_manifest_service_spec.rb b/spec/services/dependency_proxy/find_or_create_manifest_service_spec.rb
index b3f88f91289..ef608c9b113 100644
--- a/spec/services/dependency_proxy/find_or_create_manifest_service_spec.rb
+++ b/spec/services/dependency_proxy/find_or_create_manifest_service_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe DependencyProxy::FindOrCreateManifestService do
let(:token) { Digest::SHA256.hexdigest('123') }
let(:headers) do
{
- 'docker-content-digest' => dependency_proxy_manifest.digest,
+ DependencyProxy::Manifest::DIGEST_HEADER => dependency_proxy_manifest.digest,
'content-type' => dependency_proxy_manifest.content_type
}
end
@@ -31,6 +31,14 @@ RSpec.describe DependencyProxy::FindOrCreateManifestService do
end
end
+ shared_examples 'returning no manifest' do
+ it 'returns a nil manifest' do
+ expect(subject[:status]).to eq(:success)
+ expect(subject[:from_cache]).to eq false
+ expect(subject[:manifest]).to be_nil
+ end
+ end
+
context 'when no manifest exists' do
let_it_be(:image) { 'new-image' }
@@ -40,7 +48,15 @@ RSpec.describe DependencyProxy::FindOrCreateManifestService do
stub_manifest_download(image, tag, headers: headers)
end
- it_behaves_like 'downloading the manifest'
+ it_behaves_like 'returning no manifest'
+
+ context 'with dependency_proxy_manifest_workhorse feature disabled' do
+ before do
+ stub_feature_flags(dependency_proxy_manifest_workhorse: false)
+ end
+
+ it_behaves_like 'downloading the manifest'
+ end
end
context 'failed head request' do
@@ -49,7 +65,15 @@ RSpec.describe DependencyProxy::FindOrCreateManifestService do
stub_manifest_download(image, tag, headers: headers)
end
- it_behaves_like 'downloading the manifest'
+ it_behaves_like 'returning no manifest'
+
+ context 'with dependency_proxy_manifest_workhorse feature disabled' do
+ before do
+ stub_feature_flags(dependency_proxy_manifest_workhorse: false)
+ end
+
+ it_behaves_like 'downloading the manifest'
+ end
end
end
@@ -60,7 +84,7 @@ RSpec.describe DependencyProxy::FindOrCreateManifestService do
shared_examples 'using the cached manifest' do
it 'uses cached manifest instead of downloading one', :aggregate_failures do
- expect { subject }.to change { dependency_proxy_manifest.reload.updated_at }
+ expect { subject }.to change { dependency_proxy_manifest.reload.read_at }
expect(subject[:status]).to eq(:success)
expect(subject[:manifest]).to be_a(DependencyProxy::Manifest)
@@ -76,16 +100,24 @@ RSpec.describe DependencyProxy::FindOrCreateManifestService do
let(:content_type) { 'new-content-type' }
before do
- stub_manifest_head(image, tag, headers: { 'docker-content-digest' => digest, 'content-type' => content_type })
- stub_manifest_download(image, tag, headers: { 'docker-content-digest' => digest, 'content-type' => content_type })
+ stub_manifest_head(image, tag, headers: { DependencyProxy::Manifest::DIGEST_HEADER => digest, 'content-type' => content_type })
+ stub_manifest_download(image, tag, headers: { DependencyProxy::Manifest::DIGEST_HEADER => digest, 'content-type' => content_type })
end
- it 'downloads the new manifest and updates the existing record', :aggregate_failures do
- expect(subject[:status]).to eq(:success)
- expect(subject[:manifest]).to eq(dependency_proxy_manifest)
- expect(subject[:manifest].content_type).to eq(content_type)
- expect(subject[:manifest].digest).to eq(digest)
- expect(subject[:from_cache]).to eq false
+ it_behaves_like 'returning no manifest'
+
+ context 'with dependency_proxy_manifest_workhorse feature disabled' do
+ before do
+ stub_feature_flags(dependency_proxy_manifest_workhorse: false)
+ end
+
+ it 'downloads the new manifest and updates the existing record', :aggregate_failures do
+ expect(subject[:status]).to eq(:success)
+ expect(subject[:manifest]).to eq(dependency_proxy_manifest)
+ expect(subject[:manifest].content_type).to eq(content_type)
+ expect(subject[:manifest].digest).to eq(digest)
+ expect(subject[:from_cache]).to eq false
+ end
end
end
@@ -96,7 +128,15 @@ RSpec.describe DependencyProxy::FindOrCreateManifestService do
stub_manifest_download(image, tag, headers: headers)
end
- it_behaves_like 'downloading the manifest'
+ it_behaves_like 'returning no manifest'
+
+ context 'with dependency_proxy_manifest_workhorse feature disabled' do
+ before do
+ stub_feature_flags(dependency_proxy_manifest_workhorse: false)
+ end
+
+ it_behaves_like 'downloading the manifest'
+ end
end
context 'failed connection' do
diff --git a/spec/services/dependency_proxy/head_manifest_service_spec.rb b/spec/services/dependency_proxy/head_manifest_service_spec.rb
index 9c1e4d650f8..949a8eb3bee 100644
--- a/spec/services/dependency_proxy/head_manifest_service_spec.rb
+++ b/spec/services/dependency_proxy/head_manifest_service_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe DependencyProxy::HeadManifestService do
let(:content_type) { 'foo' }
let(:headers) do
{
- 'docker-content-digest' => digest,
+ DependencyProxy::Manifest::DIGEST_HEADER => digest,
'content-type' => content_type
}
end
diff --git a/spec/services/dependency_proxy/pull_manifest_service_spec.rb b/spec/services/dependency_proxy/pull_manifest_service_spec.rb
index b3053174cc0..6018a3229fb 100644
--- a/spec/services/dependency_proxy/pull_manifest_service_spec.rb
+++ b/spec/services/dependency_proxy/pull_manifest_service_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe DependencyProxy::PullManifestService do
let(:digest) { '12345' }
let(:content_type) { 'foo' }
let(:headers) do
- { 'docker-content-digest' => digest, 'content-type' => content_type }
+ { DependencyProxy::Manifest::DIGEST_HEADER => digest, 'content-type' => content_type }
end
subject { described_class.new(image, tag, token).execute_with_manifest(&method(:check_response)) }
diff --git a/spec/services/deployments/archive_in_project_service_spec.rb b/spec/services/deployments/archive_in_project_service_spec.rb
new file mode 100644
index 00000000000..d4039ee7b4a
--- /dev/null
+++ b/spec/services/deployments/archive_in_project_service_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Deployments::ArchiveInProjectService do
+ let_it_be(:project) { create(:project, :repository) }
+
+ let(:service) { described_class.new(project, nil) }
+
+ describe '#execute' do
+ subject { service.execute }
+
+ context 'when there are archivable deployments' do
+ let!(:deployments) { create_list(:deployment, 3, project: project) }
+ let!(:deployment_refs) { deployments.map(&:ref_path) }
+
+ before do
+ deployments.each(&:create_ref)
+ allow(Deployment).to receive(:archivables_in) { deployments }
+ end
+
+ it 'returns result code' do
+ expect(subject[:result]).to eq(:archived)
+ expect(subject[:status]).to eq(:success)
+ expect(subject[:count]).to eq(3)
+ end
+
+ it 'archives the deployment' do
+ expect(deployments.map(&:archived?)).to be_all(false)
+ expect(deployment_refs_exist?).to be_all(true)
+
+ subject
+
+ deployments.each(&:reload)
+ expect(deployments.map(&:archived?)).to be_all(true)
+ expect(deployment_refs_exist?).to be_all(false)
+ end
+
+ context 'when ref does not exist by some reason' do
+ before do
+ project.repository.delete_refs(*deployment_refs)
+ end
+
+ it 'does not raise an error' do
+ expect(deployment_refs_exist?).to be_all(false)
+
+ expect { subject }.not_to raise_error
+
+ expect(deployment_refs_exist?).to be_all(false)
+ end
+ end
+
+ context 'when deployments_archive feature flag is disabled' do
+ before do
+ stub_feature_flags(deployments_archive: false)
+ end
+
+ it 'does not do anything' do
+ expect(subject[:status]).to eq(:error)
+ expect(subject[:message]).to eq('Feature flag is not enabled')
+ end
+ end
+
+ def deployment_refs_exist?
+ deployment_refs.map { |path| project.repository.ref_exists?(path) }
+ end
+ end
+
+ context 'when there are no archivable deployments' do
+ before do
+ allow(Deployment).to receive(:archivables_in) { Deployment.none }
+ end
+
+ it 'returns result code' do
+ expect(subject[:result]).to eq(:empty)
+ expect(subject[:status]).to eq(:success)
+ end
+ end
+ end
+end
diff --git a/spec/services/deployments/link_merge_requests_service_spec.rb b/spec/services/deployments/link_merge_requests_service_spec.rb
index a5a13230d6f..62adc834733 100644
--- a/spec/services/deployments/link_merge_requests_service_spec.rb
+++ b/spec/services/deployments/link_merge_requests_service_spec.rb
@@ -32,6 +32,19 @@ RSpec.describe Deployments::LinkMergeRequestsService do
end
end
+ context 'when the deployment is for one of the production environments' do
+ it 'links merge requests' do
+ environment =
+ create(:environment, environment_type: 'production', name: 'production/gcp')
+
+ deploy = create(:deployment, :success, environment: environment)
+
+ expect(deploy).to receive(:link_merge_requests).once
+
+ described_class.new(deploy).execute
+ end
+ end
+
context 'when the deployment failed' do
it 'does nothing' do
environment = create(:environment, name: 'foo')
diff --git a/spec/services/emails/create_service_spec.rb b/spec/services/emails/create_service_spec.rb
index 1396a1fce30..2fabf4ae66a 100644
--- a/spec/services/emails/create_service_spec.rb
+++ b/spec/services/emails/create_service_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
RSpec.describe Emails::CreateService do
- let(:user) { create(:user) }
+ let_it_be(:user) { create(:user) }
+
let(:opts) { { email: 'new@email.com', user: user } }
subject(:service) { described_class.new(user, opts) }
@@ -22,7 +23,7 @@ RSpec.describe Emails::CreateService do
it 'has the right user association' do
service.execute
- expect(user.emails).to eq(Email.where(opts))
+ expect(user.emails).to include(Email.find_by(opts))
end
end
end
diff --git a/spec/services/emails/destroy_service_spec.rb b/spec/services/emails/destroy_service_spec.rb
index f8407be41e7..7dcf367016e 100644
--- a/spec/services/emails/destroy_service_spec.rb
+++ b/spec/services/emails/destroy_service_spec.rb
@@ -15,5 +15,15 @@ RSpec.describe Emails::DestroyService do
expect(user.emails).not_to include(email)
expect(response).to be true
end
+
+ context 'when it corresponds to the user primary email' do
+ let(:email) { user.emails.find_by!(email: user.email) }
+
+ it 'does not remove the email and raises an exception' do
+ expect { service.execute(email) }.to raise_error(StandardError, 'Cannot delete primary email')
+
+ expect(user.emails).to include(email)
+ end
+ end
end
end
diff --git a/spec/services/error_tracking/collect_error_service_spec.rb b/spec/services/error_tracking/collect_error_service_spec.rb
index ee9d0813e64..52d095148c8 100644
--- a/spec/services/error_tracking/collect_error_service_spec.rb
+++ b/spec/services/error_tracking/collect_error_service_spec.rb
@@ -4,7 +4,8 @@ require 'spec_helper'
RSpec.describe ErrorTracking::CollectErrorService do
let_it_be(:project) { create(:project) }
- let_it_be(:parsed_event) { Gitlab::Json.parse(fixture_file('error_tracking/parsed_event.json')) }
+ let_it_be(:parsed_event_file) { 'error_tracking/parsed_event.json' }
+ let_it_be(:parsed_event) { Gitlab::Json.parse(fixture_file(parsed_event_file)) }
subject { described_class.new(project, nil, event: parsed_event) }
@@ -41,6 +42,14 @@ RSpec.describe ErrorTracking::CollectErrorService do
expect(event.payload).to eq parsed_event
end
+ context 'python sdk event' do
+ let(:parsed_event) { Gitlab::Json.parse(fixture_file('error_tracking/python_event.json')) }
+
+ it 'creates a valid event' do
+ expect { subject.execute }.to change { ErrorTracking::ErrorEvent.count }.by(1)
+ end
+ end
+
context 'unusual payload' do
let(:modified_event) { parsed_event }
@@ -64,5 +73,25 @@ RSpec.describe ErrorTracking::CollectErrorService do
end
end
end
+
+ context 'go payload' do
+ let(:parsed_event) { Gitlab::Json.parse(fixture_file('error_tracking/go_parsed_event.json')) }
+
+ it 'has correct values set' do
+ subject.execute
+
+ event = ErrorTracking::ErrorEvent.last
+ error = event.error
+
+ expect(error.name).to eq '*errors.errorString'
+ expect(error.description).to start_with 'Hello world'
+ expect(error.platform).to eq 'go'
+
+ expect(event.description).to start_with 'Hello world'
+ expect(event.level).to eq 'error'
+ expect(event.environment).to eq 'Accumulate'
+ expect(event.payload).to eq parsed_event
+ end
+ end
end
end
diff --git a/spec/services/google_cloud/service_accounts_service_spec.rb b/spec/services/google_cloud/service_accounts_service_spec.rb
new file mode 100644
index 00000000000..a0d09affa72
--- /dev/null
+++ b/spec/services/google_cloud/service_accounts_service_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GoogleCloud::ServiceAccountsService do
+ let_it_be(:project) { create(:project) }
+
+ let(:service) { described_class.new(project) }
+
+ describe 'find_for_project' do
+ context 'when a project does not have GCP service account vars' do
+ before do
+ project.variables.build(key: 'blah', value: 'foo', environment_scope: 'world')
+ project.save!
+ end
+
+ it 'returns an empty list' do
+ expect(service.find_for_project.length).to eq(0)
+ end
+ end
+
+ context 'when a project has GCP service account ci vars' do
+ before do
+ project.variables.build(environment_scope: '*', key: 'GCP_PROJECT_ID', value: 'prj1')
+ project.variables.build(environment_scope: '*', key: 'GCP_SERVICE_ACCOUNT_KEY', value: 'mock')
+ project.variables.build(environment_scope: 'staging', key: 'GCP_PROJECT_ID', value: 'prj2')
+ project.variables.build(environment_scope: 'staging', key: 'GCP_SERVICE_ACCOUNT', value: 'mock')
+ project.variables.build(environment_scope: 'production', key: 'GCP_PROJECT_ID', value: 'prj3')
+ project.variables.build(environment_scope: 'production', key: 'GCP_SERVICE_ACCOUNT', value: 'mock')
+ project.variables.build(environment_scope: 'production', key: 'GCP_SERVICE_ACCOUNT_KEY', value: 'mock')
+ project.save!
+ end
+
+ it 'returns a list of service accounts' do
+ list = service.find_for_project
+
+ aggregate_failures 'testing list of service accounts' do
+ expect(list.length).to eq(3)
+
+ expect(list.first[:environment]).to eq('*')
+ expect(list.first[:gcp_project]).to eq('prj1')
+ expect(list.first[:service_account_exists]).to eq(false)
+ expect(list.first[:service_account_key_exists]).to eq(true)
+
+ expect(list.second[:environment]).to eq('staging')
+ expect(list.second[:gcp_project]).to eq('prj2')
+ expect(list.second[:service_account_exists]).to eq(true)
+ expect(list.second[:service_account_key_exists]).to eq(false)
+
+ expect(list.third[:environment]).to eq('production')
+ expect(list.third[:gcp_project]).to eq('prj3')
+ expect(list.third[:service_account_exists]).to eq(true)
+ expect(list.third[:service_account_key_exists]).to eq(true)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb
index bcba39b0eb4..7ea08131419 100644
--- a/spec/services/groups/create_service_spec.rb
+++ b/spec/services/groups/create_service_spec.rb
@@ -171,7 +171,7 @@ RSpec.describe Groups::CreateService, '#execute' do
context 'with an active group-level integration' do
let(:service) { described_class.new(user, group_params.merge(parent_id: group.id)) }
- let!(:group_integration) { create(:prometheus_integration, group: group, project: nil, api_url: 'https://prometheus.group.com/') }
+ let!(:group_integration) { create(:prometheus_integration, :group, group: group, api_url: 'https://prometheus.group.com/') }
let(:group) do
create(:group).tap do |group|
group.add_owner(user)
@@ -186,7 +186,7 @@ RSpec.describe Groups::CreateService, '#execute' do
context 'with an active subgroup' do
let(:service) { described_class.new(user, group_params.merge(parent_id: subgroup.id)) }
- let!(:subgroup_integration) { create(:prometheus_integration, group: subgroup, project: nil, api_url: 'https://prometheus.subgroup.com/') }
+ let!(:subgroup_integration) { create(:prometheus_integration, :group, group: subgroup, api_url: 'https://prometheus.subgroup.com/') }
let(:subgroup) do
create(:group, parent: group).tap do |subgroup|
subgroup.add_owner(user)
@@ -242,4 +242,41 @@ RSpec.describe Groups::CreateService, '#execute' do
end
end
end
+
+ describe 'invite team email' do
+ let(:service) { described_class.new(user, group_params) }
+
+ before do
+ allow(Namespaces::InviteTeamEmailWorker).to receive(:perform_in)
+ end
+
+ it 'is sent' do
+ group = service.execute
+ delay = Namespaces::InviteTeamEmailService::DELIVERY_DELAY_IN_MINUTES
+ expect(Namespaces::InviteTeamEmailWorker).to have_received(:perform_in).with(delay, group.id, user.id)
+ end
+
+ context 'when group has not been persisted' do
+ let(:service) { described_class.new(user, group_params.merge(name: '<script>alert("Attack!")</script>')) }
+
+ it 'not sent' do
+ expect(Namespaces::InviteTeamEmailWorker).not_to receive(:perform_in)
+ service.execute
+ end
+ end
+
+ context 'when group is not root' do
+ let(:parent_group) { create :group }
+ let(:service) { described_class.new(user, group_params.merge(parent_id: parent_group.id)) }
+
+ before do
+ parent_group.add_owner(user)
+ end
+
+ it 'not sent' do
+ expect(Namespaces::InviteTeamEmailWorker).not_to receive(:perform_in)
+ service.execute
+ end
+ end
+ end
end
diff --git a/spec/services/groups/import_export/import_service_spec.rb b/spec/services/groups/import_export/import_service_spec.rb
index ad5c4364deb..292f2e2b86b 100644
--- a/spec/services/groups/import_export/import_service_spec.rb
+++ b/spec/services/groups/import_export/import_service_spec.rb
@@ -7,6 +7,10 @@ RSpec.describe Groups::ImportExport::ImportService do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
+ before do
+ allow(GroupImportWorker).to receive(:with_status).and_return(GroupImportWorker)
+ end
+
context 'when the job can be successfully scheduled' do
subject(:import_service) { described_class.new(group: group, user: user) }
@@ -20,6 +24,8 @@ RSpec.describe Groups::ImportExport::ImportService do
end
it 'enqueues an import job' do
+ allow(GroupImportWorker).to receive(:with_status).and_return(GroupImportWorker)
+
expect(GroupImportWorker).to receive(:perform_async).with(user.id, group.id)
import_service.async_execute
diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb
index 8b506d2bc2c..35d46884f4d 100644
--- a/spec/services/groups/transfer_service_spec.rb
+++ b/spec/services/groups/transfer_service_spec.rb
@@ -153,7 +153,7 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
it 'adds an error on group' do
transfer_service.execute(nil)
- expect(transfer_service.error).to eq('Transfer failed: The parent group already has a subgroup with the same path.')
+ expect(transfer_service.error).to eq('Transfer failed: The parent group already has a subgroup or a project with the same path.')
end
end
@@ -185,9 +185,7 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
context 'when projects have project namespaces' do
let_it_be(:project1) { create(:project, :private, namespace: group) }
- let_it_be(:project_namespace1) { create(:project_namespace, project: project1) }
let_it_be(:project2) { create(:project, :private, namespace: group) }
- let_it_be(:project_namespace2) { create(:project_namespace, project: project2) }
it_behaves_like 'project namespace path is in sync with project path' do
let(:group_full_path) { "#{group.path}" }
@@ -241,7 +239,7 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
it 'adds an error on group' do
transfer_service.execute(new_parent_group)
- expect(transfer_service.error).to eq('Transfer failed: The parent group already has a subgroup with the same path.')
+ expect(transfer_service.error).to eq('Transfer failed: The parent group already has a subgroup or a project with the same path.')
end
end
@@ -250,36 +248,45 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
let_it_be(:membership) { create(:group_member, :owner, group: new_parent_group, user: user) }
let_it_be(:project) { create(:project, path: 'foo', namespace: new_parent_group) }
- before do
- group.update_attribute(:path, 'foo')
- end
-
- it 'returns false' do
- expect(transfer_service.execute(new_parent_group)).to be_falsy
- end
-
it 'adds an error on group' do
- transfer_service.execute(new_parent_group)
- expect(transfer_service.error).to eq('Transfer failed: Validation failed: Group URL has already been taken')
+ expect(transfer_service.execute(new_parent_group)).to be_falsy
+ expect(transfer_service.error).to eq('Transfer failed: The parent group already has a subgroup or a project with the same path.')
end
- context 'when projects have project namespaces' do
- let!(:project_namespace) { create(:project_namespace, project: project) }
-
+ # currently when a project is created it gets a corresponding project namespace
+ # so we test the case where a project without a project namespace is transferred
+ # for backward compatibility
+ context 'without project namespace' do
before do
- transfer_service.execute(new_parent_group)
+ project_namespace = project.project_namespace
+ project.update_column(:project_namespace_id, nil)
+ project_namespace.delete
end
- it_behaves_like 'project namespace path is in sync with project path' do
- let(:group_full_path) { "#{new_parent_group.full_path}" }
- let(:projects_with_project_namespace) { [project] }
+ it 'adds an error on group' do
+ expect(project.reload.project_namespace).to be_nil
+ expect(transfer_service.execute(new_parent_group)).to be_falsy
+ expect(transfer_service.error).to eq('Transfer failed: Validation failed: Group URL has already been taken')
end
end
end
+ context 'when projects have project namespaces' do
+ let_it_be(:project) { create(:project, path: 'foo', namespace: new_parent_group) }
+
+ before do
+ transfer_service.execute(new_parent_group)
+ end
+
+ it_behaves_like 'project namespace path is in sync with project path' do
+ let(:group_full_path) { "#{new_parent_group.full_path}" }
+ let(:projects_with_project_namespace) { [project] }
+ end
+ end
+
context 'when the group is allowed to be transferred' do
let_it_be(:new_parent_group, reload: true) { create(:group, :public) }
- let_it_be(:new_parent_group_integration) { create(:integrations_slack, group: new_parent_group, project: nil, webhook: 'http://new-group.slack.com') }
+ let_it_be(:new_parent_group_integration) { create(:integrations_slack, :group, group: new_parent_group, webhook: 'http://new-group.slack.com') }
before do
allow(PropagateIntegrationWorker).to receive(:perform_async)
@@ -316,7 +323,7 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
context 'with an inherited integration' do
let_it_be(:instance_integration) { create(:integrations_slack, :instance, webhook: 'http://project.slack.com') }
- let_it_be(:group_integration) { create(:integrations_slack, group: group, project: nil, webhook: 'http://group.slack.com', inherit_from_id: instance_integration.id) }
+ let_it_be(:group_integration) { create(:integrations_slack, :group, group: group, webhook: 'http://group.slack.com', inherit_from_id: instance_integration.id) }
it 'replaces inherited integrations', :aggregate_failures do
expect(new_created_integration.webhook).to eq(new_parent_group_integration.webhook)
@@ -326,7 +333,7 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
end
context 'with a custom integration' do
- let_it_be(:group_integration) { create(:integrations_slack, group: group, project: nil, webhook: 'http://group.slack.com') }
+ let_it_be(:group_integration) { create(:integrations_slack, :group, group: group, webhook: 'http://group.slack.com') }
it 'does not updates the integrations', :aggregate_failures do
expect { transfer_service.execute(new_parent_group) }.not_to change { group_integration.webhook }
@@ -445,8 +452,6 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
context 'when transferring a group with project descendants' do
let!(:project1) { create(:project, :repository, :private, namespace: group) }
let!(:project2) { create(:project, :repository, :internal, namespace: group) }
- let!(:project_namespace1) { create(:project_namespace, project: project1) }
- let!(:project_namespace2) { create(:project_namespace, project: project2) }
before do
TestEnv.clean_test_path
@@ -483,8 +488,6 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
let!(:project1) { create(:project, :repository, :public, namespace: group) }
let!(:project2) { create(:project, :repository, :public, namespace: group) }
let!(:new_parent_group) { create(:group, :private) }
- let!(:project_namespace1) { create(:project_namespace, project: project1) }
- let!(:project_namespace2) { create(:project_namespace, project: project2) }
it 'updates projects visibility to match the new parent' do
group.projects.each do |project|
@@ -504,8 +507,6 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
let!(:project2) { create(:project, :repository, :internal, namespace: group) }
let!(:subgroup1) { create(:group, :private, parent: group) }
let!(:subgroup2) { create(:group, :internal, parent: group) }
- let!(:project_namespace1) { create(:project_namespace, project: project1) }
- let!(:project_namespace2) { create(:project_namespace, project: project2) }
before do
TestEnv.clean_test_path
@@ -593,11 +594,16 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
let_it_be_with_reload(:group) { create(:group, :private, parent: old_parent_group) }
let_it_be(:new_group_member) { create(:user) }
let_it_be(:old_group_member) { create(:user) }
+ let_it_be(:unique_subgroup_member) { create(:user) }
+ let_it_be(:direct_project_member) { create(:user) }
before do
new_parent_group.add_maintainer(new_group_member)
old_parent_group.add_maintainer(old_group_member)
+ subgroup1.add_developer(unique_subgroup_member)
+ nested_project.add_developer(direct_project_member)
group.refresh_members_authorized_projects
+ subgroup1.refresh_members_authorized_projects
end
it 'removes old project authorizations' do
@@ -613,7 +619,7 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
end
it 'performs authorizations job immediately' do
- expect(AuthorizedProjectsWorker).to receive(:bulk_perform_inline)
+ expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to receive(:bulk_perform_inline)
transfer_service.execute(new_parent_group)
end
@@ -630,14 +636,24 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
ProjectAuthorization.where(project_id: nested_project.id, user_id: new_group_member.id).size
}.from(0).to(1)
end
+
+ it 'preserves existing project authorizations for direct project members' do
+ expect { transfer_service.execute(new_parent_group) }.not_to change {
+ ProjectAuthorization.where(project_id: nested_project.id, user_id: direct_project_member.id).count
+ }
+ end
end
- context 'for groups with many members' do
- before do
- 11.times do
- new_parent_group.add_maintainer(create(:user))
- end
+ context 'for nested groups with unique members' do
+ it 'preserves existing project authorizations' do
+ expect { transfer_service.execute(new_parent_group) }.not_to change {
+ ProjectAuthorization.where(project_id: nested_project.id, user_id: unique_subgroup_member.id).count
+ }
end
+ end
+
+ context 'for groups with many projects' do
+ let_it_be(:project_list) { create_list(:project, 11, :repository, :private, namespace: group) }
it 'adds new project authorizations for the user which makes a transfer' do
transfer_service.execute(new_parent_group)
@@ -646,9 +662,21 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
expect(ProjectAuthorization.where(project_id: nested_project.id, user_id: user.id).size).to eq(1)
end
+ it 'adds project authorizations for users in the new hierarchy' do
+ expect { transfer_service.execute(new_parent_group) }.to change {
+ ProjectAuthorization.where(project_id: project_list.map { |project| project.id }, user_id: new_group_member.id).size
+ }.from(0).to(project_list.count)
+ end
+
+ it 'removes project authorizations for users in the old hierarchy' do
+ expect { transfer_service.execute(new_parent_group) }.to change {
+ ProjectAuthorization.where(project_id: project_list.map { |project| project.id }, user_id: old_group_member.id).size
+ }.from(project_list.count).to(0)
+ end
+
it 'schedules authorizations job' do
- expect(AuthorizedProjectsWorker).to receive(:bulk_perform_async)
- .with(array_including(new_parent_group.members_with_parents.pluck(:user_id).map {|id| [id, anything] }))
+ expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to receive(:bulk_perform_async)
+ .with(array_including(group.all_projects.ids.map { |id| [id, anything] }))
transfer_service.execute(new_parent_group)
end
diff --git a/spec/services/import/github/notes/create_service_spec.rb b/spec/services/import/github/notes/create_service_spec.rb
new file mode 100644
index 00000000000..57699def848
--- /dev/null
+++ b/spec/services/import/github/notes/create_service_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Import::Github::Notes::CreateService do
+ it 'does not support quick actions' do
+ project = create(:project, :repository)
+ user = create(:user)
+ merge_request = create(:merge_request, source_project: project)
+
+ project.add_maintainer(user)
+
+ note = described_class.new(
+ project,
+ user,
+ note: '/close',
+ noteable_type: 'MergeRequest',
+ noteable_id: merge_request.id
+ ).execute
+
+ expect(note.note).to eq('/close')
+ expect(note.noteable.closed?).to be(false)
+ end
+end
diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb
index b96dd981e0f..cf75efb5c57 100644
--- a/spec/services/issues/build_service_spec.rb
+++ b/spec/services/issues/build_service_spec.rb
@@ -7,12 +7,14 @@ RSpec.describe Issues::BuildService do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:developer) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
let_it_be(:guest) { create(:user) }
let(:user) { developer }
before_all do
project.add_developer(developer)
+ project.add_reporter(reporter)
project.add_guest(guest)
end
@@ -140,76 +142,64 @@ RSpec.describe Issues::BuildService do
end
describe '#execute' do
- context 'as developer' do
- it 'builds a new issues with given params' do
- milestone = create(:milestone, project: project)
- issue = build_issue(milestone_id: milestone.id)
-
- expect(issue.milestone).to eq(milestone)
- expect(issue.issue_type).to eq('issue')
- expect(issue.work_item_type.base_type).to eq('issue')
- end
+ describe 'setting milestone' do
+ context 'when developer' do
+ it 'builds a new issues with given params' do
+ milestone = create(:milestone, project: project)
+ issue = build_issue(milestone_id: milestone.id)
+
+ expect(issue.milestone).to eq(milestone)
+ end
- it 'sets milestone to nil if it is not available for the project' do
- milestone = create(:milestone, project: create(:project))
- issue = build_issue(milestone_id: milestone.id)
+ it 'sets milestone to nil if it is not available for the project' do
+ milestone = create(:milestone, project: create(:project))
+ issue = build_issue(milestone_id: milestone.id)
- expect(issue.milestone).to be_nil
+ expect(issue.milestone).to be_nil
+ end
end
- context 'when issue_type is incident' do
- it 'sets the correct issue type' do
- issue = build_issue(issue_type: 'incident')
+ context 'when guest' do
+ let(:user) { guest }
- expect(issue.issue_type).to eq('incident')
- expect(issue.work_item_type.base_type).to eq('incident')
+ it 'cannot set milestone' do
+ milestone = create(:milestone, project: project)
+ issue = build_issue(milestone_id: milestone.id)
+
+ expect(issue.milestone).to be_nil
end
end
end
- context 'as guest' do
- let(:user) { guest }
-
- it 'cannot set milestone' do
- milestone = create(:milestone, project: project)
- issue = build_issue(milestone_id: milestone.id)
+ describe 'setting issue type' do
+ context 'with a corresponding WorkItem::Type' do
+ let_it_be(:type_issue_id) { WorkItem::Type.default_issue_type.id }
+ let_it_be(:type_incident_id) { WorkItem::Type.default_by_type(:incident).id }
+
+ where(:issue_type, :current_user, :work_item_type_id, :resulting_issue_type) do
+ nil | ref(:guest) | ref(:type_issue_id) | 'issue'
+ 'issue' | ref(:guest) | ref(:type_issue_id) | 'issue'
+ 'incident' | ref(:guest) | ref(:type_issue_id) | 'issue'
+ 'incident' | ref(:reporter) | ref(:type_incident_id) | 'incident'
+ # update once support for test_case is enabled
+ 'test_case' | ref(:guest) | ref(:type_issue_id) | 'issue'
+ # update once support for requirement is enabled
+ 'requirement' | ref(:guest) | ref(:type_issue_id) | 'issue'
+ 'invalid' | ref(:guest) | ref(:type_issue_id) | 'issue'
+ # ensure that we don't set a value which has a permission check but is an invalid issue type
+ 'project' | ref(:guest) | ref(:type_issue_id) | 'issue'
+ end
- expect(issue.milestone).to be_nil
- end
+ with_them do
+ let(:user) { current_user }
- context 'setting issue type' do
- shared_examples 'builds an issue' do
- specify do
+ it 'builds an issue' do
issue = build_issue(issue_type: issue_type)
expect(issue.issue_type).to eq(resulting_issue_type)
expect(issue.work_item_type_id).to eq(work_item_type_id)
end
end
-
- it 'cannot set invalid issue type' do
- issue = build_issue(issue_type: 'project')
-
- expect(issue).to be_issue
- end
-
- context 'with a corresponding WorkItem::Type' do
- let_it_be(:type_issue_id) { WorkItem::Type.default_issue_type.id }
- let_it_be(:type_incident_id) { WorkItem::Type.default_by_type(:incident).id }
-
- where(:issue_type, :work_item_type_id, :resulting_issue_type) do
- nil | ref(:type_issue_id) | 'issue'
- 'issue' | ref(:type_issue_id) | 'issue'
- 'incident' | ref(:type_incident_id) | 'incident'
- 'test_case' | ref(:type_issue_id) | 'issue' # update once support for test_case is enabled
- 'requirement' | ref(:type_issue_id) | 'issue' # update once support for requirement is enabled
- 'invalid' | ref(:type_issue_id) | 'issue'
- end
-
- with_them do
- it_behaves_like 'builds an issue'
- end
- end
end
end
end
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index 93ef046a632..158f9dec83e 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -83,6 +83,14 @@ RSpec.describe Issues::CloseService do
service.execute(issue)
end
+ it 'does not change escalation status' do
+ resolved = IncidentManagement::Escalatable::STATUSES[:resolved]
+
+ expect { service.execute(issue) }
+ .to not_change { IncidentManagement::IssuableEscalationStatus.where(issue: issue).count }
+ .and not_change { IncidentManagement::IssuableEscalationStatus.where(status: resolved).count }
+ end
+
context 'issue is incident type' do
let(:issue) { create(:incident, project: project) }
let(:current_user) { user }
@@ -90,6 +98,40 @@ RSpec.describe Issues::CloseService do
subject { service.execute(issue) }
it_behaves_like 'an incident management tracked event', :incident_management_incident_closed
+
+ it 'creates a new escalation resolved escalation status', :aggregate_failures do
+ expect { service.execute(issue) }.to change { IncidentManagement::IssuableEscalationStatus.where(issue: issue).count }.by(1)
+
+ expect(issue.incident_management_issuable_escalation_status).to be_resolved
+ end
+
+ context 'when there is an escalation status' do
+ before do
+ create(:incident_management_issuable_escalation_status, issue: issue)
+ end
+
+ it 'changes escalations status to resolved' do
+ expect { service.execute(issue) }.to change { issue.incident_management_issuable_escalation_status.reload.resolved? }.to(true)
+ end
+
+ it 'adds a system note', :aggregate_failures do
+ expect { service.execute(issue) }.to change { issue.notes.count }.by(1)
+
+ new_note = issue.notes.last
+ expect(new_note.note).to eq('changed the status to **Resolved** by closing the incident')
+ expect(new_note.author).to eq(user)
+ end
+
+ context 'when the escalation status did not change to resolved' do
+ let(:escalation_status) { instance_double('IncidentManagement::IssuableEscalationStatus', resolve: false) }
+
+ it 'does not create a system note' do
+ allow(issue).to receive(:incident_management_issuable_escalation_status).and_return(escalation_status)
+
+ expect { service.execute(issue) }.not_to change { issue.notes.count }
+ end
+ end
+ end
end
end
@@ -237,7 +279,7 @@ RSpec.describe Issues::CloseService do
it 'verifies the number of queries' do
recorded = ActiveRecord::QueryRecorder.new { close_issue }
- expected_queries = 27
+ expected_queries = 32
expect(recorded.count).to be <= expected_queries
expect(recorded.cached_count).to eq(0)
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 1887be4896e..18e03db11dc 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -15,8 +15,7 @@ RSpec.describe Issues::CreateService do
expect(described_class.rate_limiter_scoped_and_keyed).to be_a(RateLimitedService::RateLimiterScopedAndKeyed)
expect(described_class.rate_limiter_scoped_and_keyed.key).to eq(:issues_create)
- expect(described_class.rate_limiter_scoped_and_keyed.opts[:scope]).to eq(%i[project current_user])
- expect(described_class.rate_limiter_scoped_and_keyed.opts[:users_allowlist].call).to eq(%w[support-bot])
+ expect(described_class.rate_limiter_scoped_and_keyed.opts[:scope]).to eq(%i[project current_user external_author])
expect(described_class.rate_limiter_scoped_and_keyed.rate_limiter_klass).to eq(Gitlab::ApplicationRateLimiter)
end
end
@@ -81,7 +80,7 @@ RSpec.describe Issues::CreateService do
it_behaves_like 'not an incident issue'
- context 'issue is incident type' do
+ context 'when issue is incident type' do
before do
opts.merge!(issue_type: 'incident')
end
@@ -91,23 +90,37 @@ RSpec.describe Issues::CreateService do
subject { issue }
- it_behaves_like 'incident issue'
- it_behaves_like 'has incident label'
+ context 'as reporter' do
+ let_it_be(:reporter) { create(:user) }
- it 'does create an incident label' do
- expect { subject }
- .to change { Label.where(incident_label_attributes).count }.by(1)
- end
+ let(:user) { reporter }
- context 'when invalid' do
- before do
- opts.merge!(title: '')
+ before_all do
+ project.add_reporter(reporter)
end
- it 'does not apply an incident label prematurely' do
- expect { subject }.to not_change(LabelLink, :count).and not_change(Issue, :count)
+ it_behaves_like 'incident issue'
+ it_behaves_like 'has incident label'
+
+ it 'does create an incident label' do
+ expect { subject }
+ .to change { Label.where(incident_label_attributes).count }.by(1)
+ end
+
+ context 'when invalid' do
+ before do
+ opts.merge!(title: '')
+ end
+
+ it 'does not apply an incident label prematurely' do
+ expect { subject }.to not_change(LabelLink, :count).and not_change(Issue, :count)
+ end
end
end
+
+ context 'as guest' do
+ it_behaves_like 'not an incident issue'
+ end
end
it 'refreshes the number of open issues', :use_clean_rails_memory_store_caching do
@@ -289,6 +302,44 @@ RSpec.describe Issues::CreateService do
described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
end
+ context 'when rate limiting is in effect', :freeze_time, :clean_gitlab_redis_rate_limiting do
+ let(:user) { create(:user) }
+
+ before do
+ stub_feature_flags(rate_limited_service_issues_create: true)
+ stub_application_setting(issues_create_limit: 1)
+ end
+
+ subject do
+ 2.times { described_class.new(project: project, current_user: user, params: opts, spam_params: double).execute }
+ end
+
+ context 'when too many requests are sent by one user' do
+ it 'raises an error' do
+ expect do
+ subject
+ end.to raise_error(RateLimitedService::RateLimitedError)
+ end
+
+ it 'creates 1 issue' do
+ expect do
+ subject
+ rescue RateLimitedService::RateLimitedError
+ end.to change { Issue.count }.by(1)
+ end
+ end
+
+ context 'when limit is higher than count of issues being created' do
+ before do
+ stub_application_setting(issues_create_limit: 2)
+ end
+
+ it 'creates 2 issues' do
+ expect { subject }.to change { Issue.count }.by(2)
+ end
+ end
+ end
+
context 'after_save callback to store_mentions' do
context 'when mentionable attributes change' do
let(:opts) { { title: 'Title', description: "Description with #{user.to_reference}" } }
diff --git a/spec/services/issues/set_crm_contacts_service_spec.rb b/spec/services/issues/set_crm_contacts_service_spec.rb
new file mode 100644
index 00000000000..65b22fe3b35
--- /dev/null
+++ b/spec/services/issues/set_crm_contacts_service_spec.rb
@@ -0,0 +1,162 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Issues::SetCrmContactsService do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:contacts) { create_list(:contact, 4, group: group) }
+
+ let(:issue) { create(:issue, project: project) }
+ let(:does_not_exist_or_no_permission) { "The resource that you are attempting to access does not exist or you don't have permission to perform this action" }
+
+ before do
+ create(:issue_customer_relations_contact, issue: issue, contact: contacts[0])
+ create(:issue_customer_relations_contact, issue: issue, contact: contacts[1])
+ end
+
+ subject(:set_crm_contacts) do
+ described_class.new(project: project, current_user: user, params: params).execute(issue)
+ end
+
+ describe '#execute' do
+ context 'when the user has no permission' do
+ let(:params) { { crm_contact_ids: [contacts[1].id, contacts[2].id] } }
+
+ it 'returns expected error response' do
+ response = set_crm_contacts
+
+ expect(response).to be_error
+ expect(response.message).to match_array(['You have insufficient permissions to set customer relations contacts for this issue'])
+ end
+ end
+
+ context 'when user has permission' do
+ before do
+ group.add_reporter(user)
+ end
+
+ context 'when the contact does not exist' do
+ let(:params) { { crm_contact_ids: [non_existing_record_id] } }
+
+ it 'returns expected error response' do
+ response = set_crm_contacts
+
+ expect(response).to be_error
+ expect(response.message).to match_array(["Issue customer relations contacts #{non_existing_record_id}: #{does_not_exist_or_no_permission}"])
+ end
+ end
+
+ context 'when the contact belongs to a different group' do
+ let(:group2) { create(:group) }
+ let(:contact) { create(:contact, group: group2) }
+ let(:params) { { crm_contact_ids: [contact.id] } }
+
+ before do
+ group2.add_reporter(user)
+ end
+
+ it 'returns expected error response' do
+ response = set_crm_contacts
+
+ expect(response).to be_error
+ expect(response.message).to match_array(["Issue customer relations contacts #{contact.id}: #{does_not_exist_or_no_permission}"])
+ end
+ end
+
+ context 'replace' do
+ let(:params) { { crm_contact_ids: [contacts[1].id, contacts[2].id] } }
+
+ it 'updates the issue with correct contacts' do
+ response = set_crm_contacts
+
+ expect(response).to be_success
+ expect(issue.customer_relations_contacts).to match_array([contacts[1], contacts[2]])
+ end
+ end
+
+ context 'add' do
+ let(:params) { { add_crm_contact_ids: [contacts[3].id] } }
+
+ it 'updates the issue with correct contacts' do
+ response = set_crm_contacts
+
+ expect(response).to be_success
+ expect(issue.customer_relations_contacts).to match_array([contacts[0], contacts[1], contacts[3]])
+ end
+ end
+
+ context 'remove' do
+ let(:params) { { remove_crm_contact_ids: [contacts[0].id] } }
+
+ it 'updates the issue with correct contacts' do
+ response = set_crm_contacts
+
+ expect(response).to be_success
+ expect(issue.customer_relations_contacts).to match_array([contacts[1]])
+ end
+ end
+
+ context 'when attempting to add more than 6' do
+ let(:id) { contacts[0].id }
+ let(:params) { { add_crm_contact_ids: [id, id, id, id, id, id, id] } }
+
+ it 'returns expected error message' do
+ response = set_crm_contacts
+
+ expect(response).to be_error
+ expect(response.message).to match_array(['You can only add up to 6 contacts at one time'])
+ end
+ end
+
+ context 'when trying to remove non-existent contact' do
+ let(:params) { { remove_crm_contact_ids: [non_existing_record_id] } }
+
+ it 'returns expected error message' do
+ response = set_crm_contacts
+
+ expect(response).to be_success
+ expect(response.message).to be_nil
+ end
+ end
+
+ context 'when combining params' do
+ let(:error_invalid_params) { 'You cannot combine crm_contact_ids with add_crm_contact_ids or remove_crm_contact_ids' }
+
+ context 'add and remove' do
+ let(:params) { { remove_crm_contact_ids: [contacts[1].id], add_crm_contact_ids: [contacts[3].id] } }
+
+ it 'updates the issue with correct contacts' do
+ response = set_crm_contacts
+
+ expect(response).to be_success
+ expect(issue.customer_relations_contacts).to match_array([contacts[0], contacts[3]])
+ end
+ end
+
+ context 'replace and remove' do
+ let(:params) { { crm_contact_ids: [contacts[3].id], remove_crm_contact_ids: [contacts[0].id] } }
+
+ it 'returns expected error response' do
+ response = set_crm_contacts
+
+ expect(response).to be_error
+ expect(response.message).to match_array([error_invalid_params])
+ end
+ end
+
+ context 'replace and add' do
+ let(:params) { { crm_contact_ids: [contacts[3].id], add_crm_contact_ids: [contacts[1].id] } }
+
+ it 'returns expected error response' do
+ response = set_crm_contacts
+
+ expect(response).to be_error
+ expect(response.message).to match_array([error_invalid_params])
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 83c17f051eb..85b8fef685e 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -1256,28 +1256,38 @@ RSpec.describe Issues::UpdateService, :mailer do
let(:closed_issuable) { create(:closed_issue, project: project) }
end
- context 'real-time updates' do
- using RSpec::Parameterized::TableSyntax
-
+ context 'broadcasting issue assignee updates' do
let(:update_params) { { assignee_ids: [user2.id] } }
- where(:action_cable_in_app_enabled, :feature_flag_enabled, :should_broadcast) do
- true | true | true
- true | false | true
- false | true | true
- false | false | false
- end
+ context 'when feature flag is enabled' do
+ before do
+ stub_feature_flags(broadcast_issue_updates: true)
+ end
+
+ it 'triggers the GraphQL subscription' do
+ expect(GraphqlTriggers).to receive(:issuable_assignees_updated).with(issue)
+
+ update_issue(update_params)
+ end
- with_them do
- it 'broadcasts to the issues channel based on ActionCable and feature flag values' do
- allow(Gitlab::ActionCable::Config).to receive(:in_app?).and_return(action_cable_in_app_enabled)
- stub_feature_flags(broadcast_issue_updates: feature_flag_enabled)
+ context 'when assignee is not updated' do
+ let(:update_params) { { title: 'Some other title' } }
- if should_broadcast
- expect(GraphqlTriggers).to receive(:issuable_assignees_updated).with(issue)
- else
+ it 'does not trigger the GraphQL subscription' do
expect(GraphqlTriggers).not_to receive(:issuable_assignees_updated).with(issue)
+
+ update_issue(update_params)
end
+ end
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(broadcast_issue_updates: false)
+ end
+
+ it 'does not trigger the GraphQL subscription' do
+ expect(GraphqlTriggers).not_to receive(:issuable_assignees_updated).with(issue)
update_issue(update_params)
end
diff --git a/spec/services/labels/transfer_service_spec.rb b/spec/services/labels/transfer_service_spec.rb
index 18fd401f383..05190accb33 100644
--- a/spec/services/labels/transfer_service_spec.rb
+++ b/spec/services/labels/transfer_service_spec.rb
@@ -3,107 +3,121 @@
require 'spec_helper'
RSpec.describe Labels::TransferService do
- describe '#execute' do
- let_it_be(:user) { create(:user) }
+ shared_examples 'transfer labels' do
+ describe '#execute' do
+ let_it_be(:user) { create(:user) }
- let_it_be(:old_group_ancestor) { create(:group) }
- let_it_be(:old_group) { create(:group, parent: old_group_ancestor) }
+ let_it_be(:old_group_ancestor) { create(:group) }
+ let_it_be(:old_group) { create(:group, parent: old_group_ancestor) }
- let_it_be(:new_group) { create(:group) }
+ let_it_be(:new_group) { create(:group) }
- let_it_be(:project) { create(:project, :repository, group: new_group) }
+ let_it_be(:project) { create(:project, :repository, group: new_group) }
- subject(:service) { described_class.new(user, old_group, project) }
+ subject(:service) { described_class.new(user, old_group, project) }
- before do
- old_group_ancestor.add_developer(user)
- new_group.add_developer(user)
- end
+ before do
+ old_group_ancestor.add_developer(user)
+ new_group.add_developer(user)
+ end
- it 'recreates missing group labels at project level and assigns them to the issuables' do
- old_group_label_1 = create(:group_label, group: old_group)
- old_group_label_2 = create(:group_label, group: old_group)
+ it 'recreates missing group labels at project level and assigns them to the issuables' do
+ old_group_label_1 = create(:group_label, group: old_group)
+ old_group_label_2 = create(:group_label, group: old_group)
- labeled_issue = create(:labeled_issue, project: project, labels: [old_group_label_1])
- labeled_merge_request = create(:labeled_merge_request, source_project: project, labels: [old_group_label_2])
+ labeled_issue = create(:labeled_issue, project: project, labels: [old_group_label_1])
+ labeled_merge_request = create(:labeled_merge_request, source_project: project, labels: [old_group_label_2])
- expect { service.execute }.to change(project.labels, :count).by(2)
- expect(labeled_issue.reload.labels).to contain_exactly(project.labels.find_by_title(old_group_label_1.title))
- expect(labeled_merge_request.reload.labels).to contain_exactly(project.labels.find_by_title(old_group_label_2.title))
- end
+ expect { service.execute }.to change(project.labels, :count).by(2)
+ expect(labeled_issue.reload.labels).to contain_exactly(project.labels.find_by_title(old_group_label_1.title))
+ expect(labeled_merge_request.reload.labels).to contain_exactly(project.labels.find_by_title(old_group_label_2.title))
+ end
- it 'recreates missing ancestor group labels at project level and assigns them to the issuables' do
- old_group_ancestor_label_1 = create(:group_label, group: old_group_ancestor)
- old_group_ancestor_label_2 = create(:group_label, group: old_group_ancestor)
+ it 'recreates missing ancestor group labels at project level and assigns them to the issuables' do
+ old_group_ancestor_label_1 = create(:group_label, group: old_group_ancestor)
+ old_group_ancestor_label_2 = create(:group_label, group: old_group_ancestor)
- labeled_issue = create(:labeled_issue, project: project, labels: [old_group_ancestor_label_1])
- labeled_merge_request = create(:labeled_merge_request, source_project: project, labels: [old_group_ancestor_label_2])
+ labeled_issue = create(:labeled_issue, project: project, labels: [old_group_ancestor_label_1])
+ labeled_merge_request = create(:labeled_merge_request, source_project: project, labels: [old_group_ancestor_label_2])
- expect { service.execute }.to change(project.labels, :count).by(2)
- expect(labeled_issue.reload.labels).to contain_exactly(project.labels.find_by_title(old_group_ancestor_label_1.title))
- expect(labeled_merge_request.reload.labels).to contain_exactly(project.labels.find_by_title(old_group_ancestor_label_2.title))
- end
+ expect { service.execute }.to change(project.labels, :count).by(2)
+ expect(labeled_issue.reload.labels).to contain_exactly(project.labels.find_by_title(old_group_ancestor_label_1.title))
+ expect(labeled_merge_request.reload.labels).to contain_exactly(project.labels.find_by_title(old_group_ancestor_label_2.title))
+ end
- it 'recreates label priorities related to the missing group labels' do
- old_group_label = create(:group_label, group: old_group)
- create(:labeled_issue, project: project, labels: [old_group_label])
- create(:label_priority, project: project, label: old_group_label, priority: 1)
+ it 'recreates label priorities related to the missing group labels' do
+ old_group_label = create(:group_label, group: old_group)
+ create(:labeled_issue, project: project, labels: [old_group_label])
+ create(:label_priority, project: project, label: old_group_label, priority: 1)
- service.execute
+ service.execute
- new_project_label = project.labels.find_by(title: old_group_label.title)
- expect(new_project_label.id).not_to eq old_group_label.id
- expect(new_project_label.priorities).not_to be_empty
- end
+ new_project_label = project.labels.find_by(title: old_group_label.title)
+ expect(new_project_label.id).not_to eq old_group_label.id
+ expect(new_project_label.priorities).not_to be_empty
+ end
- it 'does not recreate missing group labels that are not applied to issues or merge requests' do
- old_group_label = create(:group_label, group: old_group)
+ it 'does not recreate missing group labels that are not applied to issues or merge requests' do
+ old_group_label = create(:group_label, group: old_group)
- service.execute
+ service.execute
- expect(project.labels.where(title: old_group_label.title)).to be_empty
- end
+ expect(project.labels.where(title: old_group_label.title)).to be_empty
+ end
- it 'does not recreate missing group labels that already exist in the project group' do
- old_group_label = create(:group_label, group: old_group)
- labeled_issue = create(:labeled_issue, project: project, labels: [old_group_label])
+ it 'does not recreate missing group labels that already exist in the project group' do
+ old_group_label = create(:group_label, group: old_group)
+ labeled_issue = create(:labeled_issue, project: project, labels: [old_group_label])
- new_group_label = create(:group_label, group: new_group, title: old_group_label.title)
+ new_group_label = create(:group_label, group: new_group, title: old_group_label.title)
- service.execute
+ service.execute
- expect(project.labels.where(title: old_group_label.title)).to be_empty
- expect(labeled_issue.reload.labels).to contain_exactly(new_group_label)
- end
+ expect(project.labels.where(title: old_group_label.title)).to be_empty
+ expect(labeled_issue.reload.labels).to contain_exactly(new_group_label)
+ end
- it 'updates only label links in the given project' do
- old_group_label = create(:group_label, group: old_group)
- other_project = create(:project, group: old_group)
+ it 'updates only label links in the given project' do
+ old_group_label = create(:group_label, group: old_group)
+ other_project = create(:project, group: old_group)
- labeled_issue = create(:labeled_issue, project: project, labels: [old_group_label])
- other_project_labeled_issue = create(:labeled_issue, project: other_project, labels: [old_group_label])
+ labeled_issue = create(:labeled_issue, project: project, labels: [old_group_label])
+ other_project_labeled_issue = create(:labeled_issue, project: other_project, labels: [old_group_label])
- service.execute
+ service.execute
- expect(labeled_issue.reload.labels).not_to include(old_group_label)
- expect(other_project_labeled_issue.reload.labels).to contain_exactly(old_group_label)
- end
+ expect(labeled_issue.reload.labels).not_to include(old_group_label)
+ expect(other_project_labeled_issue.reload.labels).to contain_exactly(old_group_label)
+ end
- context 'when moving within the same ancestor group' do
- let(:other_subgroup) { create(:group, parent: old_group_ancestor) }
- let(:project) { create(:project, :repository, group: other_subgroup) }
+ context 'when moving within the same ancestor group' do
+ let(:other_subgroup) { create(:group, parent: old_group_ancestor) }
+ let(:project) { create(:project, :repository, group: other_subgroup) }
- it 'does not recreate ancestor group labels' do
- old_group_ancestor_label_1 = create(:group_label, group: old_group_ancestor)
- old_group_ancestor_label_2 = create(:group_label, group: old_group_ancestor)
+ it 'does not recreate ancestor group labels' do
+ old_group_ancestor_label_1 = create(:group_label, group: old_group_ancestor)
+ old_group_ancestor_label_2 = create(:group_label, group: old_group_ancestor)
- labeled_issue = create(:labeled_issue, project: project, labels: [old_group_ancestor_label_1])
- labeled_merge_request = create(:labeled_merge_request, source_project: project, labels: [old_group_ancestor_label_2])
+ labeled_issue = create(:labeled_issue, project: project, labels: [old_group_ancestor_label_1])
+ labeled_merge_request = create(:labeled_merge_request, source_project: project, labels: [old_group_ancestor_label_2])
- expect { service.execute }.not_to change(project.labels, :count)
- expect(labeled_issue.reload.labels).to contain_exactly(old_group_ancestor_label_1)
- expect(labeled_merge_request.reload.labels).to contain_exactly(old_group_ancestor_label_2)
+ expect { service.execute }.not_to change(project.labels, :count)
+ expect(labeled_issue.reload.labels).to contain_exactly(old_group_ancestor_label_1)
+ expect(labeled_merge_request.reload.labels).to contain_exactly(old_group_ancestor_label_2)
+ end
end
end
end
+
+ context 'with use_optimized_group_labels_query FF on' do
+ it_behaves_like 'transfer labels'
+ end
+
+ context 'with use_optimized_group_labels_query FF off' do
+ before do
+ stub_feature_flags(use_optimized_group_labels_query: false)
+ end
+
+ it_behaves_like 'transfer labels'
+ end
end
diff --git a/spec/services/loose_foreign_keys/batch_cleaner_service_spec.rb b/spec/services/loose_foreign_keys/batch_cleaner_service_spec.rb
new file mode 100644
index 00000000000..bdb3d0f6700
--- /dev/null
+++ b/spec/services/loose_foreign_keys/batch_cleaner_service_spec.rb
@@ -0,0 +1,119 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe LooseForeignKeys::BatchCleanerService do
+ include MigrationsHelpers
+
+ def create_table_structure
+ migration = ActiveRecord::Migration.new.extend(Gitlab::Database::MigrationHelpers::LooseForeignKeyHelpers)
+
+ migration.create_table :_test_loose_fk_parent_table
+
+ migration.create_table :_test_loose_fk_child_table_1 do |t|
+ t.bigint :parent_id
+ end
+
+ migration.create_table :_test_loose_fk_child_table_2 do |t|
+ t.bigint :parent_id_with_different_column
+ end
+
+ migration.track_record_deletions(:_test_loose_fk_parent_table)
+ end
+
+ let(:parent_model) do
+ Class.new(ApplicationRecord) do
+ self.table_name = '_test_loose_fk_parent_table'
+
+ include LooseForeignKey
+
+ loose_foreign_key :_test_loose_fk_child_table_1, :parent_id, on_delete: :async_delete
+ loose_foreign_key :_test_loose_fk_child_table_2, :parent_id_with_different_column, on_delete: :async_nullify
+ end
+ end
+
+ let(:child_model_1) do
+ Class.new(ApplicationRecord) do
+ self.table_name = '_test_loose_fk_child_table_1'
+ end
+ end
+
+ let(:child_model_2) do
+ Class.new(ApplicationRecord) do
+ self.table_name = '_test_loose_fk_child_table_2'
+ end
+ end
+
+ let(:loose_fk_child_table_1) { table(:_test_loose_fk_child_table_1) }
+ let(:loose_fk_child_table_2) { table(:_test_loose_fk_child_table_2) }
+ let(:parent_record_1) { parent_model.create! }
+ let(:other_parent_record) { parent_model.create! }
+
+ before(:all) do
+ create_table_structure
+ end
+
+ before do
+ parent_record_1
+
+ loose_fk_child_table_1.create!(parent_id: parent_record_1.id)
+ loose_fk_child_table_1.create!(parent_id: parent_record_1.id)
+
+ # these will not be deleted
+ loose_fk_child_table_1.create!(parent_id: other_parent_record.id)
+ loose_fk_child_table_1.create!(parent_id: other_parent_record.id)
+
+ loose_fk_child_table_2.create!(parent_id_with_different_column: parent_record_1.id)
+ loose_fk_child_table_2.create!(parent_id_with_different_column: parent_record_1.id)
+
+ # these will not be deleted
+ loose_fk_child_table_2.create!(parent_id_with_different_column: other_parent_record.id)
+ loose_fk_child_table_2.create!(parent_id_with_different_column: other_parent_record.id)
+ end
+
+ after(:all) do
+ migration = ActiveRecord::Migration.new
+ migration.drop_table :_test_loose_fk_parent_table
+ migration.drop_table :_test_loose_fk_child_table_1
+ migration.drop_table :_test_loose_fk_child_table_2
+ end
+
+ context 'when parent records are deleted' do
+ let(:deleted_records_counter) { Gitlab::Metrics.registry.get(:loose_foreign_key_processed_deleted_records) }
+
+ before do
+ parent_record_1.delete
+
+ expect(loose_fk_child_table_1.count).to eq(4)
+ expect(loose_fk_child_table_2.count).to eq(4)
+
+ described_class.new(parent_klass: parent_model,
+ deleted_parent_records: LooseForeignKeys::DeletedRecord.status_pending.all,
+ models_by_table_name: {
+ '_test_loose_fk_child_table_1' => child_model_1,
+ '_test_loose_fk_child_table_2' => child_model_2
+ }).execute
+ end
+
+ it 'cleans up the child records' do
+ expect(loose_fk_child_table_1.where(parent_id: parent_record_1.id)).to be_empty
+ expect(loose_fk_child_table_2.where(parent_id_with_different_column: nil).count).to eq(2)
+ end
+
+ it 'cleans up the pending parent DeletedRecord' do
+ expect(LooseForeignKeys::DeletedRecord.status_pending.count).to eq(0)
+ expect(LooseForeignKeys::DeletedRecord.status_processed.count).to eq(1)
+ end
+
+ it 'records the DeletedRecord status updates', :prometheus do
+ counter = Gitlab::Metrics.registry.get(:loose_foreign_key_processed_deleted_records)
+
+ expect(counter.get(table: parent_model.table_name, db_config_name: 'main')).to eq(1)
+ end
+
+ it 'does not delete unrelated records' do
+ expect(loose_fk_child_table_1.where(parent_id: other_parent_record.id).count).to eq(2)
+ expect(loose_fk_child_table_2.where(parent_id_with_different_column: other_parent_record.id).count).to eq(2)
+ end
+ end
+end
diff --git a/spec/services/loose_foreign_keys/cleaner_service_spec.rb b/spec/services/loose_foreign_keys/cleaner_service_spec.rb
new file mode 100644
index 00000000000..6f37ac49435
--- /dev/null
+++ b/spec/services/loose_foreign_keys/cleaner_service_spec.rb
@@ -0,0 +1,147 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe LooseForeignKeys::CleanerService do
+ let(:schema) { ApplicationRecord.connection.current_schema }
+ let(:deleted_records) do
+ [
+ LooseForeignKeys::DeletedRecord.new(fully_qualified_table_name: "#{schema}.projects", primary_key_value: non_existing_record_id),
+ LooseForeignKeys::DeletedRecord.new(fully_qualified_table_name: "#{schema}.projects", primary_key_value: non_existing_record_id)
+ ]
+ end
+
+ let(:loose_fk_definition) do
+ ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(
+ 'projects',
+ 'issues',
+ {
+ column: 'project_id',
+ on_delete: :async_nullify
+ }
+ )
+ end
+
+ subject(:cleaner_service) do
+ described_class.new(
+ model: Issue,
+ foreign_key_definition: loose_fk_definition,
+ deleted_parent_records: deleted_records
+ )
+ end
+
+ context 'when invalid foreign key definition is passed' do
+ context 'when invalid on_delete argument was given' do
+ before do
+ loose_fk_definition.options[:on_delete] = :invalid
+ end
+
+ it 'raises KeyError' do
+ expect { cleaner_service.execute }.to raise_error(StandardError, /Invalid on_delete argument/)
+ end
+ end
+ end
+
+ describe 'query generation' do
+ context 'when single primary key is used' do
+ let(:issue) { create(:issue) }
+
+ let(:deleted_records) do
+ [
+ LooseForeignKeys::DeletedRecord.new(fully_qualified_table_name: "#{schema}.projects", primary_key_value: issue.project_id)
+ ]
+ end
+
+ it 'generates an IN query for nullifying the rows' do
+ expected_query = %{UPDATE "issues" SET "project_id" = NULL WHERE ("issues"."id") IN (SELECT "issues"."id" FROM "issues" WHERE "issues"."project_id" IN (#{issue.project_id}) LIMIT 500)}
+ expect(ApplicationRecord.connection).to receive(:execute).with(expected_query).and_call_original
+
+ cleaner_service.execute
+
+ issue.reload
+ expect(issue.project_id).to be_nil
+ end
+
+ it 'generates an IN query for deleting the rows' do
+ loose_fk_definition.options[:on_delete] = :async_delete
+
+ expected_query = %{DELETE FROM "issues" WHERE ("issues"."id") IN (SELECT "issues"."id" FROM "issues" WHERE "issues"."project_id" IN (#{issue.project_id}) LIMIT 1000)}
+ expect(ApplicationRecord.connection).to receive(:execute).with(expected_query).and_call_original
+
+ cleaner_service.execute
+
+ expect(Issue.exists?(id: issue.id)).to eq(false)
+ end
+ end
+
+ context 'when composite primary key is used' do
+ let!(:user) { create(:user) }
+ let!(:project) { create(:project) }
+
+ let(:loose_fk_definition) do
+ ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(
+ 'users',
+ 'project_authorizations',
+ {
+ column: 'user_id',
+ on_delete: :async_delete
+ }
+ )
+ end
+
+ let(:deleted_records) do
+ [
+ LooseForeignKeys::DeletedRecord.new(fully_qualified_table_name: "#{schema}.users", primary_key_value: user.id)
+ ]
+ end
+
+ subject(:cleaner_service) do
+ described_class.new(
+ model: ProjectAuthorization,
+ foreign_key_definition: loose_fk_definition,
+ deleted_parent_records: deleted_records
+ )
+ end
+
+ before do
+ project.add_developer(user)
+ end
+
+ it 'generates an IN query for deleting the rows' do
+ expected_query = %{DELETE FROM "project_authorizations" WHERE ("project_authorizations"."user_id", "project_authorizations"."project_id", "project_authorizations"."access_level") IN (SELECT "project_authorizations"."user_id", "project_authorizations"."project_id", "project_authorizations"."access_level" FROM "project_authorizations" WHERE "project_authorizations"."user_id" IN (#{user.id}) LIMIT 1000)}
+ expect(ApplicationRecord.connection).to receive(:execute).with(expected_query).and_call_original
+
+ cleaner_service.execute
+
+ expect(ProjectAuthorization.exists?(user_id: user.id)).to eq(false)
+ end
+
+ context 'when the query generation is incorrect (paranoid check)' do
+ it 'raises error if the foreign key condition is missing' do
+ expect_next_instance_of(LooseForeignKeys::CleanerService) do |instance|
+ expect(instance).to receive(:delete_query).and_return('wrong query')
+ end
+
+ expect { cleaner_service.execute }.to raise_error /FATAL: foreign key condition is missing from the generated query/
+ end
+ end
+ end
+
+ context 'when with_skip_locked parameter is true' do
+ subject(:cleaner_service) do
+ described_class.new(
+ model: Issue,
+ foreign_key_definition: loose_fk_definition,
+ deleted_parent_records: deleted_records,
+ with_skip_locked: true
+ )
+ end
+
+ it 'generates a query with the SKIP LOCKED clause' do
+ expect(ApplicationRecord.connection).to receive(:execute).with(/FOR UPDATE SKIP LOCKED/).and_call_original
+
+ cleaner_service.execute
+ end
+ end
+ end
+end
diff --git a/spec/services/members/create_service_spec.rb b/spec/services/members/create_service_spec.rb
index 2e6e6041fc3..fe866d73215 100644
--- a/spec/services/members/create_service_spec.rb
+++ b/spec/services/members/create_service_spec.rb
@@ -196,4 +196,108 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
end
end
end
+
+ context 'when assigning tasks to be done' do
+ let(:additional_params) do
+ { invite_source: '_invite_source_', tasks_to_be_done: %w(ci code), tasks_project_id: source.id }
+ end
+
+ before do
+ stub_experiments(invite_members_for_task: true)
+ end
+
+ it 'creates 2 task issues', :aggregate_failures do
+ expect(TasksToBeDone::CreateWorker)
+ .to receive(:perform_async)
+ .with(anything, user.id, [member.id])
+ .once
+ .and_call_original
+ expect { execute_service }.to change { source.issues.count }.by(2)
+
+ expect(source.issues).to all have_attributes(
+ project: source,
+ author: user
+ )
+ end
+
+ context 'when passing many user ids' do
+ before do
+ stub_licensed_features(multiple_issue_assignees: false)
+ end
+
+ let(:another_user) { create(:user) }
+ let(:user_ids) { [member.id, another_user.id].join(',') }
+
+ it 'still creates 2 task issues', :aggregate_failures do
+ expect(TasksToBeDone::CreateWorker)
+ .to receive(:perform_async)
+ .with(anything, user.id, array_including(member.id, another_user.id))
+ .once
+ .and_call_original
+ expect { execute_service }.to change { source.issues.count }.by(2)
+
+ expect(source.issues).to all have_attributes(
+ project: source,
+ author: user
+ )
+ end
+ end
+
+ context 'when a `tasks_project_id` is missing' do
+ let(:additional_params) do
+ { invite_source: '_invite_source_', tasks_to_be_done: %w(ci code) }
+ end
+
+ it 'does not create task issues' do
+ expect(TasksToBeDone::CreateWorker).not_to receive(:perform_async)
+ execute_service
+ end
+ end
+
+ context 'when `tasks_to_be_done` are missing' do
+ let(:additional_params) do
+ { invite_source: '_invite_source_', tasks_project_id: source.id }
+ end
+
+ it 'does not create task issues' do
+ expect(TasksToBeDone::CreateWorker).not_to receive(:perform_async)
+ execute_service
+ end
+ end
+
+ context 'when invalid `tasks_to_be_done` are passed' do
+ let(:additional_params) do
+ { invite_source: '_invite_source_', tasks_project_id: source.id, tasks_to_be_done: %w(invalid_task) }
+ end
+
+ it 'does not create task issues' do
+ expect(TasksToBeDone::CreateWorker).not_to receive(:perform_async)
+ execute_service
+ end
+ end
+
+ context 'when invalid `tasks_project_id` is passed' do
+ let(:another_project) { create(:project) }
+ let(:additional_params) do
+ { invite_source: '_invite_source_', tasks_project_id: another_project.id, tasks_to_be_done: %w(ci code) }
+ end
+
+ it 'does not create task issues' do
+ expect(TasksToBeDone::CreateWorker).not_to receive(:perform_async)
+ execute_service
+ end
+ end
+
+ context 'when a member was already invited' do
+ let(:user_ids) { create(:project_member, :invited, project: source).invite_email }
+ let(:additional_params) do
+ { invite_source: '_invite_source_', tasks_project_id: source.id, tasks_to_be_done: %w(ci code) }
+ end
+
+ it 'does not create task issues' do
+ expect(TasksToBeDone::CreateWorker).not_to receive(:perform_async)
+ execute_service
+ end
+ end
+ end
end
diff --git a/spec/services/members/invite_service_spec.rb b/spec/services/members/invite_service_spec.rb
index 478733e8aa0..7b9ae19f038 100644
--- a/spec/services/members/invite_service_spec.rb
+++ b/spec/services/members/invite_service_spec.rb
@@ -22,6 +22,11 @@ RSpec.describe Members::InviteService, :aggregate_failures, :clean_gitlab_redis_
end
it_behaves_like 'records an onboarding progress action', :user_added
+
+ it 'does not create task issues' do
+ expect(TasksToBeDone::CreateWorker).not_to receive(:perform_async)
+ expect { result }.not_to change { project.issues.count }
+ end
end
context 'when email belongs to an existing user as a secondary email' do
diff --git a/spec/services/merge_requests/mergeability/run_checks_service_spec.rb b/spec/services/merge_requests/mergeability/run_checks_service_spec.rb
index 170d99f4642..71ad23bc68c 100644
--- a/spec/services/merge_requests/mergeability/run_checks_service_spec.rb
+++ b/spec/services/merge_requests/mergeability/run_checks_service_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do
let_it_be(:merge_request) { create(:merge_request) }
describe '#CHECKS' do
- it 'contains every subclass of the base checks service' do
+ it 'contains every subclass of the base checks service', :eager_load do
expect(described_class::CHECKS).to contain_exactly(*MergeRequests::Mergeability::CheckBaseService.subclasses)
end
end
@@ -19,7 +19,7 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do
let(:params) { {} }
let(:success_result) { Gitlab::MergeRequests::Mergeability::CheckResult.success }
- context 'when every check is skipped' do
+ context 'when every check is skipped', :eager_load do
before do
MergeRequests::Mergeability::CheckBaseService.subclasses.each do |subclass|
expect_next_instance_of(subclass) do |service|
diff --git a/spec/services/merge_requests/retarget_chain_service_spec.rb b/spec/services/merge_requests/retarget_chain_service_spec.rb
index 87bde4a1400..187dd0cf589 100644
--- a/spec/services/merge_requests/retarget_chain_service_spec.rb
+++ b/spec/services/merge_requests/retarget_chain_service_spec.rb
@@ -45,14 +45,6 @@ RSpec.describe MergeRequests::RetargetChainService do
.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
end
context 'in the same project' do
diff --git a/spec/services/merge_requests/toggle_attention_requested_service_spec.rb b/spec/services/merge_requests/toggle_attention_requested_service_spec.rb
new file mode 100644
index 00000000000..a26b1be529e
--- /dev/null
+++ b/spec/services/merge_requests/toggle_attention_requested_service_spec.rb
@@ -0,0 +1,128 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe MergeRequests::ToggleAttentionRequestedService do
+ let(:current_user) { create(:user) }
+ let(:user) { create(:user) }
+ let(:assignee_user) { create(:user) }
+ let(:merge_request) { create(:merge_request, reviewers: [user], assignees: [assignee_user]) }
+ let(:reviewer) { merge_request.find_reviewer(user) }
+ let(:assignee) { merge_request.find_assignee(assignee_user) }
+ let(:project) { merge_request.project }
+ let(:service) { described_class.new(project: project, current_user: current_user, merge_request: merge_request, user: user) }
+ let(:result) { service.execute }
+ let(:todo_service) { spy('todo service') }
+
+ before do
+ allow(service).to receive(:todo_service).and_return(todo_service)
+
+ project.add_developer(current_user)
+ project.add_developer(user)
+ end
+
+ describe '#execute' do
+ context 'invalid permissions' do
+ let(:service) { described_class.new(project: project, current_user: create(:user), merge_request: merge_request, user: user) }
+
+ it 'returns an error' do
+ expect(result[:status]).to eq :error
+ end
+ end
+
+ context 'reviewer does not exist' do
+ let(:service) { described_class.new(project: project, current_user: current_user, merge_request: merge_request, user: create(:user)) }
+
+ it 'returns an error' do
+ expect(result[:status]).to eq :error
+ end
+ end
+
+ context 'reviewer exists' do
+ before do
+ reviewer.update!(state: :reviewed)
+ end
+
+ it 'returns success' do
+ expect(result[:status]).to eq :success
+ end
+
+ it 'updates reviewers state' do
+ service.execute
+ reviewer.reload
+
+ expect(reviewer.state).to eq 'attention_requested'
+ end
+
+ it 'creates a new todo for the reviewer' do
+ expect(todo_service).to receive(:create_attention_requested_todo).with(merge_request, current_user, user)
+
+ service.execute
+ end
+ end
+
+ context 'assignee exists' do
+ let(:service) { described_class.new(project: project, current_user: current_user, merge_request: merge_request, user: assignee_user) }
+
+ before do
+ assignee.update!(state: :reviewed)
+ end
+
+ it 'returns success' do
+ expect(result[:status]).to eq :success
+ end
+
+ it 'updates assignees state' do
+ service.execute
+ assignee.reload
+
+ expect(assignee.state).to eq 'attention_requested'
+ end
+
+ it 'creates a new todo for the reviewer' do
+ expect(todo_service).to receive(:create_attention_requested_todo).with(merge_request, current_user, assignee_user)
+
+ service.execute
+ end
+ end
+
+ context 'assignee is the same as reviewer' do
+ let(:merge_request) { create(:merge_request, reviewers: [user], assignees: [user]) }
+ let(:service) { described_class.new(project: project, current_user: current_user, merge_request: merge_request, user: user) }
+ let(:assignee) { merge_request.find_assignee(user) }
+
+ before do
+ reviewer.update!(state: :reviewed)
+ assignee.update!(state: :reviewed)
+ end
+
+ it 'updates reviewers and assignees state' do
+ service.execute
+ reviewer.reload
+ assignee.reload
+
+ expect(reviewer.state).to eq 'attention_requested'
+ expect(assignee.state).to eq 'attention_requested'
+ end
+ end
+
+ context 'state is attention_requested' do
+ before do
+ reviewer.update!(state: :attention_requested)
+ end
+
+ it 'toggles state to reviewed' do
+ service.execute
+ reviewer.reload
+
+ expect(reviewer.state).to eq "reviewed"
+ end
+
+ it 'does not create a new todo for the reviewer' do
+ expect(todo_service).not_to receive(:create_attention_requested_todo).with(merge_request, current_user, assignee_user)
+
+ service.execute
+ end
+ end
+ end
+end
diff --git a/spec/services/namespaces/in_product_marketing_email_records_spec.rb b/spec/services/namespaces/in_product_marketing_email_records_spec.rb
new file mode 100644
index 00000000000..e5f1b275f9c
--- /dev/null
+++ b/spec/services/namespaces/in_product_marketing_email_records_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Namespaces::InProductMarketingEmailRecords do
+ let_it_be(:user) { create :user }
+
+ subject(:records) { described_class.new }
+
+ it 'initializes records' do
+ expect(subject.records).to match_array []
+ end
+
+ describe '#save!' do
+ before do
+ allow(Users::InProductMarketingEmail).to receive(:bulk_insert!)
+
+ records.add(user, :invite_team, 0)
+ records.add(user, :create, 1)
+ end
+
+ it 'bulk inserts added records' do
+ expect(Users::InProductMarketingEmail).to receive(:bulk_insert!).with(records.records)
+ records.save!
+ end
+
+ it 'resets its records' do
+ records.save!
+ expect(records.records).to match_array []
+ end
+ end
+
+ describe '#add' do
+ it 'adds a Users::InProductMarketingEmail record to its records' do
+ freeze_time do
+ records.add(user, :invite_team, 0)
+ records.add(user, :create, 1)
+
+ first, second = records.records
+
+ expect(first).to be_a Users::InProductMarketingEmail
+ expect(first.track.to_sym).to eq :invite_team
+ expect(first.series).to eq 0
+ expect(first.created_at).to eq Time.zone.now
+ expect(first.updated_at).to eq Time.zone.now
+
+ expect(second).to be_a Users::InProductMarketingEmail
+ expect(second.track.to_sym).to eq :create
+ expect(second.series).to eq 1
+ expect(second.created_at).to eq Time.zone.now
+ expect(second.updated_at).to eq Time.zone.now
+ end
+ end
+ end
+end
diff --git a/spec/services/namespaces/invite_team_email_service_spec.rb b/spec/services/namespaces/invite_team_email_service_spec.rb
new file mode 100644
index 00000000000..60ba91f433d
--- /dev/null
+++ b/spec/services/namespaces/invite_team_email_service_spec.rb
@@ -0,0 +1,128 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Namespaces::InviteTeamEmailService do
+ let_it_be(:user) { create(:user, email_opted_in: true) }
+
+ let(:track) { described_class::TRACK }
+ let(:series) { 0 }
+
+ let(:setup_for_company) { true }
+ let(:parent_group) { nil }
+ let(:group) { create(:group, parent: parent_group) }
+
+ subject(:action) { described_class.send_email(user, group) }
+
+ before do
+ group.add_owner(user)
+ allow(group).to receive(:setup_for_company).and_return(setup_for_company)
+ allow(Notify).to receive(:in_product_marketing_email).and_return(double(deliver_later: nil))
+ end
+
+ RSpec::Matchers.define :send_invite_team_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
+
+ shared_examples 'unexperimented' do
+ it { is_expected.not_to send_invite_team_email }
+
+ it 'does not record sent email' do
+ expect { subject }.not_to change { Users::InProductMarketingEmail.count }
+ end
+ end
+
+ shared_examples 'candidate' do
+ it { is_expected.to send_invite_team_email(user.id, group.id, track, 0) }
+
+ it 'records sent email' do
+ expect { subject }.to change { Users::InProductMarketingEmail.count }.by(1)
+
+ expect(
+ Users::InProductMarketingEmail.where(
+ user: user,
+ track: track,
+ series: 0
+ )
+ ).to exist
+ end
+
+ it_behaves_like 'tracks assignment and records the subject', :invite_team_email, :group do
+ subject { group }
+ end
+ end
+
+ context 'when group is in control path' do
+ before do
+ stub_experiments(invite_team_email: :control)
+ end
+
+ it { is_expected.not_to send_invite_team_email }
+
+ it 'does not record sent email' do
+ expect { subject }.not_to change { Users::InProductMarketingEmail.count }
+ end
+
+ it_behaves_like 'tracks assignment and records the subject', :invite_team_email, :group do
+ subject { group }
+ end
+ end
+
+ context 'when group is in candidate path' do
+ before do
+ stub_experiments(invite_team_email: :candidate)
+ end
+
+ it_behaves_like 'candidate'
+
+ context 'when the user has not opted into marketing emails' do
+ let(:user) { create(:user, email_opted_in: false ) }
+
+ it_behaves_like 'unexperimented'
+ end
+
+ context 'when group is not top level' do
+ it_behaves_like 'unexperimented' do
+ let(:parent_group) do
+ create(:group).tap { |g| g.add_owner(user) }
+ end
+ end
+ end
+
+ context 'when group is not set up for a company' do
+ it_behaves_like 'unexperimented' do
+ let(:setup_for_company) { nil }
+ end
+ end
+
+ context 'when other users have already been added to the group' do
+ before do
+ group.add_developer(create(:user))
+ end
+
+ it_behaves_like 'unexperimented'
+ end
+
+ context 'when other users have already been invited to the group' do
+ before do
+ group.add_developer('not_a_user_yet@example.com')
+ end
+
+ it_behaves_like 'unexperimented'
+ end
+
+ context 'when the user already got sent the email' do
+ before do
+ create(:in_product_marketing_email, user: user, track: track, series: 0)
+ end
+
+ it_behaves_like 'unexperimented'
+ end
+ end
+end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 48718cbc24a..fbf5b183365 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -3040,7 +3040,7 @@ RSpec.describe NotificationService, :mailer do
it 'emails only the creator' do
notification.pipeline_finished(pipeline)
- should_only_email(u_custom_notification_enabled, kind: :bcc)
+ should_only_email(u_custom_notification_enabled)
end
it_behaves_like 'project emails are disabled' do
@@ -3063,7 +3063,7 @@ RSpec.describe NotificationService, :mailer do
it 'sends to group notification email' do
notification.pipeline_finished(pipeline)
- expect(email_recipients(kind: :bcc).first).to eq(group_notification_email)
+ expect(email_recipients.first).to eq(group_notification_email)
end
end
end
@@ -3076,7 +3076,7 @@ RSpec.describe NotificationService, :mailer do
it 'emails only the creator' do
notification.pipeline_finished(pipeline)
- should_only_email(u_member, kind: :bcc)
+ should_only_email(u_member)
end
it_behaves_like 'project emails are disabled' do
@@ -3098,7 +3098,7 @@ RSpec.describe NotificationService, :mailer do
it 'sends to group notification email' do
notification.pipeline_finished(pipeline)
- expect(email_recipients(kind: :bcc).first).to eq(group_notification_email)
+ expect(email_recipients.first).to eq(group_notification_email)
end
end
end
@@ -3110,7 +3110,7 @@ RSpec.describe NotificationService, :mailer do
end
it 'emails only the creator' do
- should_only_email(u_watcher, kind: :bcc)
+ should_only_email(u_watcher)
end
end
@@ -3121,7 +3121,7 @@ RSpec.describe NotificationService, :mailer do
end
it 'emails only the creator' do
- should_only_email(u_custom_notification_unset, kind: :bcc)
+ should_only_email(u_custom_notification_unset)
end
end
@@ -3143,7 +3143,7 @@ RSpec.describe NotificationService, :mailer do
end
it 'emails only the creator' do
- should_only_email(u_custom_notification_enabled, kind: :bcc)
+ should_only_email(u_custom_notification_enabled)
end
end
@@ -3170,7 +3170,7 @@ RSpec.describe NotificationService, :mailer do
it 'emails only the creator' do
notification.pipeline_finished(pipeline, ref_status: ref_status)
- should_only_email(u_member, kind: :bcc)
+ should_only_email(u_member)
end
it_behaves_like 'project emails are disabled' do
@@ -3192,7 +3192,7 @@ RSpec.describe NotificationService, :mailer do
it 'sends to group notification email' do
notification.pipeline_finished(pipeline, ref_status: ref_status)
- expect(email_recipients(kind: :bcc).first).to eq(group_notification_email)
+ expect(email_recipients.first).to eq(group_notification_email)
end
end
end
@@ -3204,7 +3204,7 @@ RSpec.describe NotificationService, :mailer do
end
it 'emails only the creator' do
- should_only_email(u_watcher, kind: :bcc)
+ should_only_email(u_watcher)
end
end
@@ -3215,7 +3215,7 @@ RSpec.describe NotificationService, :mailer do
end
it 'emails only the creator' do
- should_only_email(u_custom_notification_unset, kind: :bcc)
+ should_only_email(u_custom_notification_unset)
end
end
@@ -3236,7 +3236,7 @@ RSpec.describe NotificationService, :mailer do
notification.pipeline_finished(pipeline, ref_status: ref_status)
- should_only_email(u_custom_notification_enabled, kind: :bcc)
+ should_only_email(u_custom_notification_enabled)
end
end
end
diff --git a/spec/services/packages/create_dependency_service_spec.rb b/spec/services/packages/create_dependency_service_spec.rb
index 261c6b395d5..55414ea68fe 100644
--- a/spec/services/packages/create_dependency_service_spec.rb
+++ b/spec/services/packages/create_dependency_service_spec.rb
@@ -58,9 +58,9 @@ RSpec.describe Packages::CreateDependencyService do
let_it_be(:rows) { [{ name: 'express', version_pattern: '^4.16.4' }] }
it 'creates dependences and links' do
- original_bulk_insert = ::Gitlab::Database.main.method(:bulk_insert)
- expect(::Gitlab::Database.main)
- .to receive(:bulk_insert) do |table, rows, return_ids: false, disable_quote: [], on_conflict: nil|
+ original_bulk_insert = ::ApplicationRecord.method(:legacy_bulk_insert)
+ expect(::ApplicationRecord)
+ .to receive(:legacy_bulk_insert) do |table, rows, return_ids: false, disable_quote: [], on_conflict: nil|
call_count = table == Packages::Dependency.table_name ? 2 : 1
call_count.times { original_bulk_insert.call(table, rows, return_ids: return_ids, disable_quote: disable_quote, on_conflict: on_conflict) }
end.twice
diff --git a/spec/services/packages/npm/create_package_service_spec.rb b/spec/services/packages/npm/create_package_service_spec.rb
index ba5729eaf59..b1beb2adb3b 100644
--- a/spec/services/packages/npm/create_package_service_spec.rb
+++ b/spec/services/packages/npm/create_package_service_spec.rb
@@ -16,6 +16,7 @@ RSpec.describe Packages::Npm::CreatePackageService do
let(:override) { {} }
let(:package_name) { "@#{namespace.path}/my-app" }
+ let(:version_data) { params.dig('versions', '1.0.1') }
subject { described_class.new(project, user, params).execute }
@@ -25,6 +26,7 @@ RSpec.describe Packages::Npm::CreatePackageService do
.to change { Packages::Package.count }.by(1)
.and change { Packages::Package.npm.count }.by(1)
.and change { Packages::Tag.count }.by(1)
+ .and change { Packages::Npm::Metadatum.count }.by(1)
end
it_behaves_like 'assigns the package creator' do
@@ -40,6 +42,8 @@ RSpec.describe Packages::Npm::CreatePackageService do
expect(package.version).to eq(version)
end
+ it { expect(subject.npm_metadatum.package_json).to eq(version_data) }
+
it { expect(subject.name).to eq(package_name) }
it { expect(subject.version).to eq(version) }
@@ -54,6 +58,48 @@ RSpec.describe Packages::Npm::CreatePackageService do
expect { subject }.to change { Packages::PackageFileBuildInfo.count }.by(1)
end
end
+
+ context 'with a too large metadata structure' do
+ before do
+ params[:versions][version][:test] = 'test' * 10000
+ end
+
+ it 'does not create the package' do
+ expect { subject }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Package json structure is too large')
+ .and not_change { Packages::Package.count }
+ .and not_change { Packages::Package.npm.count }
+ .and not_change { Packages::Tag.count }
+ .and not_change { Packages::Npm::Metadatum.count }
+ end
+ end
+
+ described_class::PACKAGE_JSON_NOT_ALLOWED_FIELDS.each do |field|
+ context "with not allowed #{field} field" do
+ before do
+ params[:versions][version][field] = 'test'
+ end
+
+ it 'is persisted without the field' do
+ expect { subject }
+ .to change { Packages::Package.count }.by(1)
+ .and change { Packages::Package.npm.count }.by(1)
+ .and change { Packages::Tag.count }.by(1)
+ .and change { Packages::Npm::Metadatum.count }.by(1)
+ expect(subject.npm_metadatum.package_json[field]).to be_blank
+ end
+ end
+ end
+
+ context 'with packages_npm_abbreviated_metadata disabled' do
+ before do
+ stub_feature_flags(packages_npm_abbreviated_metadata: false)
+ end
+
+ it 'creates a package without metadatum' do
+ expect { subject }
+ .not_to change { Packages::Npm::Metadatum.count }
+ end
+ end
end
describe '#execute' do
diff --git a/spec/services/packages/update_tags_service_spec.rb b/spec/services/packages/update_tags_service_spec.rb
index 6e67489fec9..c4256699c94 100644
--- a/spec/services/packages/update_tags_service_spec.rb
+++ b/spec/services/packages/update_tags_service_spec.rb
@@ -50,7 +50,7 @@ RSpec.describe Packages::UpdateTagsService do
it 'is a no op' do
expect(package).not_to receive(:tags)
- expect(::Gitlab::Database.main).not_to receive(:bulk_insert)
+ expect(::ApplicationRecord).not_to receive(:legacy_bulk_insert)
subject
end
diff --git a/spec/services/projects/all_issues_count_service_spec.rb b/spec/services/projects/all_issues_count_service_spec.rb
new file mode 100644
index 00000000000..d7e35991940
--- /dev/null
+++ b/spec/services/projects/all_issues_count_service_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::AllIssuesCountService, :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(:banned_user) { create(:user, :banned) }
+
+ subject { described_class.new(project) }
+
+ it_behaves_like 'a counter caching service'
+
+ describe '#count' do
+ it 'returns the number of all issues' do
+ create(:issue, :opened, project: project)
+ create(:issue, :opened, confidential: true, project: project)
+ create(:issue, :opened, author: banned_user, project: project)
+ create(:issue, :closed, project: project)
+
+ expect(subject.count).to eq(4)
+ end
+ end
+end
diff --git a/spec/services/projects/all_merge_requests_count_service_spec.rb b/spec/services/projects/all_merge_requests_count_service_spec.rb
new file mode 100644
index 00000000000..13954d688aa
--- /dev/null
+++ b/spec/services/projects/all_merge_requests_count_service_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::AllMergeRequestsCountService, :use_clean_rails_memory_store_caching do
+ let_it_be(:project) { create(:project) }
+
+ subject { described_class.new(project) }
+
+ it_behaves_like 'a counter caching service'
+
+ describe '#count' do
+ it 'returns the number of all merge requests' do
+ create(:merge_request,
+ :opened,
+ source_project: project,
+ target_project: project)
+ create(:merge_request,
+ :closed,
+ source_project: project,
+ target_project: project)
+ create(:merge_request,
+ :merged,
+ source_project: project,
+ target_project: project)
+
+ expect(subject.count).to eq(3)
+ end
+ end
+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 289bbf4540e..a41ba8216cc 100644
--- a/spec/services/projects/container_repository/cleanup_tags_service_spec.rb
+++ b/spec/services/projects/container_repository/cleanup_tags_service_spec.rb
@@ -41,322 +41,320 @@ RSpec.describe Projects::ContainerRepository::CleanupTagsService, :clean_gitlab_
describe '#execute' do
subject { service.execute }
- shared_examples 'reading and removing tags' do |caching_enabled: true|
- context 'when no params are specified' do
- let(:params) { {} }
+ context 'when no params are specified' do
+ let(:params) { {} }
- it 'does not remove anything' do
- expect_any_instance_of(Projects::ContainerRepository::DeleteTagsService)
- .not_to receive(:execute)
- expect_no_caching
+ it 'does not remove anything' do
+ expect_any_instance_of(Projects::ContainerRepository::DeleteTagsService)
+ .not_to receive(:execute)
+ expect_no_caching
- is_expected.to eq(expected_service_response(before_truncate_size: 0, after_truncate_size: 0, before_delete_size: 0))
- end
+ is_expected.to eq(expected_service_response(before_truncate_size: 0, after_truncate_size: 0, before_delete_size: 0))
end
+ end
- context 'when regex matching everything is specified' do
- shared_examples 'removes all matches' do
- it 'does remove all tags except latest' do
- expect_no_caching
+ context 'when regex matching everything is specified' do
+ shared_examples 'removes all matches' do
+ it 'does remove all tags except latest' do
+ expect_no_caching
- expect_delete(%w(A Ba Bb C D E))
+ expect_delete(%w(A Ba Bb C D E))
- is_expected.to eq(expected_service_response(deleted: %w(A Ba Bb C D E)))
- end
+ is_expected.to eq(expected_service_response(deleted: %w(A Ba Bb C D E)))
end
+ end
+
+ let(:params) do
+ { 'name_regex_delete' => '.*' }
+ end
+ it_behaves_like 'removes all matches'
+
+ context 'with deprecated name_regex param' do
let(:params) do
- { 'name_regex_delete' => '.*' }
+ { 'name_regex' => '.*' }
end
it_behaves_like 'removes all matches'
+ end
+ end
- context 'with deprecated name_regex param' do
- let(:params) do
- { 'name_regex' => '.*' }
- end
+ context 'with invalid regular expressions' do
+ shared_examples 'handling an invalid regex' do
+ it 'keeps all tags' do
+ expect_no_caching
- it_behaves_like 'removes all matches'
+ expect(Projects::ContainerRepository::DeleteTagsService)
+ .not_to receive(:new)
+
+ subject
end
- end
- context 'with invalid regular expressions' do
- shared_examples 'handling an invalid regex' do
- it 'keeps all tags' do
- expect_no_caching
+ it { is_expected.to eq(status: :error, message: 'invalid regex') }
- expect(Projects::ContainerRepository::DeleteTagsService)
- .not_to receive(:new)
+ it 'calls error tracking service' do
+ expect(Gitlab::ErrorTracking).to receive(:log_exception).and_call_original
- subject
- end
+ subject
+ end
+ end
- it { is_expected.to eq(status: :error, message: 'invalid regex') }
+ context 'when name_regex_delete is invalid' do
+ let(:params) { { 'name_regex_delete' => '*test*' } }
- it 'calls error tracking service' do
- expect(Gitlab::ErrorTracking).to receive(:log_exception).and_call_original
+ it_behaves_like 'handling an invalid regex'
+ end
- subject
- end
- end
+ context 'when name_regex is invalid' do
+ let(:params) { { 'name_regex' => '*test*' } }
- context 'when name_regex_delete is invalid' do
- let(:params) { { 'name_regex_delete' => '*test*' } }
+ it_behaves_like 'handling an invalid regex'
+ end
- it_behaves_like 'handling an invalid regex'
- end
+ context 'when name_regex_keep is invalid' do
+ let(:params) { { 'name_regex_keep' => '*test*' } }
- context 'when name_regex is invalid' do
- let(:params) { { 'name_regex' => '*test*' } }
+ it_behaves_like 'handling an invalid regex'
+ end
+ end
- it_behaves_like 'handling an invalid regex'
- end
+ context 'when delete regex matching specific tags is used' do
+ let(:params) do
+ { 'name_regex_delete' => 'C|D' }
+ end
- context 'when name_regex_keep is invalid' do
- let(:params) { { 'name_regex_keep' => '*test*' } }
+ it 'does remove C and D' do
+ expect_delete(%w(C D))
- it_behaves_like 'handling an invalid regex'
- end
+ expect_no_caching
+
+ is_expected.to eq(expected_service_response(deleted: %w(C D), before_truncate_size: 2, after_truncate_size: 2, before_delete_size: 2))
end
- context 'when delete regex matching specific tags is used' do
+ context 'with overriding allow regex' do
let(:params) do
- { 'name_regex_delete' => 'C|D' }
+ { 'name_regex_delete' => 'C|D',
+ 'name_regex_keep' => 'C' }
end
- it 'does remove C and D' do
- expect_delete(%w(C D))
+ it 'does not remove C' do
+ expect_delete(%w(D))
expect_no_caching
- is_expected.to eq(expected_service_response(deleted: %w(C D), before_truncate_size: 2, after_truncate_size: 2, before_delete_size: 2))
+ is_expected.to eq(expected_service_response(deleted: %w(D), before_truncate_size: 1, after_truncate_size: 1, before_delete_size: 1))
end
+ end
- context 'with overriding allow regex' do
- let(:params) do
- { 'name_regex_delete' => 'C|D',
- 'name_regex_keep' => 'C' }
- end
+ context 'with name_regex_delete overriding deprecated name_regex' do
+ let(:params) do
+ { 'name_regex' => 'C|D',
+ 'name_regex_delete' => 'D' }
+ end
- it 'does not remove C' do
- expect_delete(%w(D))
+ it 'does not remove C' do
+ expect_delete(%w(D))
- expect_no_caching
+ expect_no_caching
- is_expected.to eq(expected_service_response(deleted: %w(D), before_truncate_size: 1, after_truncate_size: 1, before_delete_size: 1))
- end
+ is_expected.to eq(expected_service_response(deleted: %w(D), before_truncate_size: 1, after_truncate_size: 1, before_delete_size: 1))
end
+ end
+ end
- context 'with name_regex_delete overriding deprecated name_regex' do
- let(:params) do
- { 'name_regex' => 'C|D',
- 'name_regex_delete' => 'D' }
- end
+ context 'with allow regex value' do
+ let(:params) do
+ { 'name_regex_delete' => '.*',
+ 'name_regex_keep' => 'B.*' }
+ end
- it 'does not remove C' do
- expect_delete(%w(D))
+ it 'does not remove B*' do
+ expect_delete(%w(A C D E))
- expect_no_caching
+ expect_no_caching
- is_expected.to eq(expected_service_response(deleted: %w(D), before_truncate_size: 1, after_truncate_size: 1, before_delete_size: 1))
- end
- end
+ is_expected.to eq(expected_service_response(deleted: %w(A C D E), before_truncate_size: 4, after_truncate_size: 4, before_delete_size: 4))
+ end
+ end
+
+ context 'when keeping only N tags' do
+ let(:params) do
+ { 'name_regex' => 'A|B.*|C',
+ 'keep_n' => 1 }
end
- context 'with allow regex value' do
- let(:params) do
- { 'name_regex_delete' => '.*',
- 'name_regex_keep' => 'B.*' }
- end
+ it 'sorts tags by date' do
+ expect_delete(%w(Bb Ba C))
- it 'does not remove B*' do
- expect_delete(%w(A C D E))
+ expect_no_caching
- expect_no_caching
+ expect(service).to receive(:order_by_date).and_call_original
- is_expected.to eq(expected_service_response(deleted: %w(A C D E), before_truncate_size: 4, after_truncate_size: 4, before_delete_size: 4))
- end
+ is_expected.to eq(expected_service_response(deleted: %w(Bb Ba C), before_truncate_size: 4, after_truncate_size: 4, before_delete_size: 3))
end
+ end
- context 'when keeping only N tags' do
- let(:params) do
- { 'name_regex' => 'A|B.*|C',
- 'keep_n' => 1 }
- end
+ context 'when not keeping N tags' do
+ let(:params) do
+ { 'name_regex' => 'A|B.*|C' }
+ end
- it 'sorts tags by date' do
- expect_delete(%w(Bb Ba C))
+ it 'does not sort tags by date' do
+ expect_delete(%w(A Ba Bb C))
- expect_no_caching
+ expect_no_caching
- expect(service).to receive(:order_by_date).and_call_original
+ expect(service).not_to receive(:order_by_date)
- is_expected.to eq(expected_service_response(deleted: %w(Bb Ba C), before_truncate_size: 4, after_truncate_size: 4, before_delete_size: 3))
- end
+ is_expected.to eq(expected_service_response(deleted: %w(A Ba Bb C), before_truncate_size: 4, after_truncate_size: 4, before_delete_size: 4))
end
+ end
- context 'when not keeping N tags' do
- let(:params) do
- { 'name_regex' => 'A|B.*|C' }
- end
-
- it 'does not sort tags by date' do
- expect_delete(%w(A Ba Bb C))
+ context 'when removing keeping only 3' do
+ let(:params) do
+ { 'name_regex_delete' => '.*',
+ 'keep_n' => 3 }
+ end
- expect_no_caching
+ it 'does remove B* and C as they are the oldest' do
+ expect_delete(%w(Bb Ba C))
- expect(service).not_to receive(:order_by_date)
+ expect_no_caching
- is_expected.to eq(expected_service_response(deleted: %w(A Ba Bb C), before_truncate_size: 4, after_truncate_size: 4, before_delete_size: 4))
- end
+ is_expected.to eq(expected_service_response(deleted: %w(Bb Ba C), before_delete_size: 3))
end
+ end
- context 'when removing keeping only 3' do
- let(:params) do
- { 'name_regex_delete' => '.*',
- 'keep_n' => 3 }
- end
+ context 'when removing older than 1 day' do
+ let(:params) do
+ { 'name_regex_delete' => '.*',
+ 'older_than' => '1 day' }
+ end
- it 'does remove B* and C as they are the oldest' do
- expect_delete(%w(Bb Ba C))
+ it 'does remove B* and C as they are older than 1 day' do
+ expect_delete(%w(Ba Bb C))
- expect_no_caching
+ expect_no_caching
- is_expected.to eq(expected_service_response(deleted: %w(Bb Ba C), before_delete_size: 3))
- end
+ is_expected.to eq(expected_service_response(deleted: %w(Ba Bb C), before_delete_size: 3))
end
+ end
- context 'when removing older than 1 day' do
- let(:params) do
- { 'name_regex_delete' => '.*',
- 'older_than' => '1 day' }
- end
+ context 'when combining all parameters' do
+ let(:params) do
+ { 'name_regex_delete' => '.*',
+ 'keep_n' => 1,
+ 'older_than' => '1 day' }
+ end
- it 'does remove B* and C as they are older than 1 day' do
- expect_delete(%w(Ba Bb C))
+ it 'does remove B* and C' do
+ expect_delete(%w(Bb Ba C))
- expect_no_caching
+ expect_no_caching
- is_expected.to eq(expected_service_response(deleted: %w(Ba Bb C), before_delete_size: 3))
- end
+ is_expected.to eq(expected_service_response(deleted: %w(Bb Ba C), before_delete_size: 3))
end
+ end
+
+ context 'when running a container_expiration_policy' do
+ let(:user) { nil }
- context 'when combining all parameters' do
+ context 'with valid container_expiration_policy param' do
let(:params) do
{ 'name_regex_delete' => '.*',
'keep_n' => 1,
- 'older_than' => '1 day' }
+ 'older_than' => '1 day',
+ 'container_expiration_policy' => true }
end
- it 'does remove B* and C' do
- expect_delete(%w(Bb Ba C))
+ it 'succeeds without a user' do
+ expect_delete(%w(Bb Ba C), container_expiration_policy: true)
- expect_no_caching
+ expect_caching
is_expected.to eq(expected_service_response(deleted: %w(Bb Ba C), before_delete_size: 3))
end
end
- context 'when running a container_expiration_policy' do
- let(:user) { nil }
-
- context 'with valid container_expiration_policy param' do
- let(:params) do
- { 'name_regex_delete' => '.*',
- 'keep_n' => 1,
- 'older_than' => '1 day',
- 'container_expiration_policy' => true }
- end
-
- it 'succeeds without a user' do
- expect_delete(%w(Bb Ba C), container_expiration_policy: true)
-
- caching_enabled ? expect_caching : expect_no_caching
-
- is_expected.to eq(expected_service_response(deleted: %w(Bb Ba C), before_delete_size: 3))
- end
+ context 'without container_expiration_policy param' do
+ let(:params) do
+ { 'name_regex_delete' => '.*',
+ 'keep_n' => 1,
+ 'older_than' => '1 day' }
end
- context 'without container_expiration_policy param' do
- let(:params) do
- { 'name_regex_delete' => '.*',
- 'keep_n' => 1,
- 'older_than' => '1 day' }
- end
-
- it 'fails' do
- is_expected.to eq(status: :error, message: 'access denied')
- end
+ it 'fails' do
+ is_expected.to eq(status: :error, message: 'access denied')
end
end
+ end
- context 'truncating the tags list' do
- let(:params) do
- {
- 'name_regex_delete' => '.*',
- 'keep_n' => 1
- }
- end
+ context 'truncating the tags list' do
+ let(:params) do
+ {
+ 'name_regex_delete' => '.*',
+ 'keep_n' => 1
+ }
+ end
- shared_examples 'returning the response' do |status:, original_size:, before_truncate_size:, after_truncate_size:, before_delete_size:|
- it 'returns the response' do
- expect_no_caching
+ shared_examples 'returning the response' do |status:, original_size:, before_truncate_size:, after_truncate_size:, before_delete_size:|
+ it 'returns the response' do
+ expect_no_caching
- result = subject
+ result = subject
- service_response = expected_service_response(
- status: status,
- original_size: original_size,
- before_truncate_size: before_truncate_size,
- after_truncate_size: after_truncate_size,
- before_delete_size: before_delete_size,
- deleted: nil
- )
+ service_response = expected_service_response(
+ status: status,
+ original_size: original_size,
+ before_truncate_size: before_truncate_size,
+ after_truncate_size: after_truncate_size,
+ before_delete_size: before_delete_size,
+ deleted: nil
+ )
- expect(result).to eq(service_response)
- end
+ expect(result).to eq(service_response)
end
+ end
- where(:feature_flag_enabled, :max_list_size, :delete_tags_service_status, :expected_status, :expected_truncated) do
- false | 10 | :success | :success | false
- false | 10 | :error | :error | false
- false | 3 | :success | :success | false
- false | 3 | :error | :error | false
- false | 0 | :success | :success | false
- false | 0 | :error | :error | false
- true | 10 | :success | :success | false
- true | 10 | :error | :error | false
- true | 3 | :success | :error | true
- true | 3 | :error | :error | true
- true | 0 | :success | :success | false
- true | 0 | :error | :error | false
- end
+ where(:feature_flag_enabled, :max_list_size, :delete_tags_service_status, :expected_status, :expected_truncated) do
+ false | 10 | :success | :success | false
+ false | 10 | :error | :error | false
+ false | 3 | :success | :success | false
+ false | 3 | :error | :error | false
+ false | 0 | :success | :success | false
+ false | 0 | :error | :error | false
+ true | 10 | :success | :success | false
+ true | 10 | :error | :error | false
+ true | 3 | :success | :error | true
+ true | 3 | :error | :error | true
+ true | 0 | :success | :success | false
+ true | 0 | :error | :error | false
+ end
- with_them do
- before do
- stub_feature_flags(container_registry_expiration_policies_throttling: feature_flag_enabled)
- stub_application_setting(container_registry_cleanup_tags_service_max_list_size: max_list_size)
- allow_next_instance_of(Projects::ContainerRepository::DeleteTagsService) do |service|
- expect(service).to receive(:execute).and_return(status: delete_tags_service_status)
- end
+ with_them do
+ before do
+ stub_feature_flags(container_registry_expiration_policies_throttling: feature_flag_enabled)
+ stub_application_setting(container_registry_cleanup_tags_service_max_list_size: max_list_size)
+ allow_next_instance_of(Projects::ContainerRepository::DeleteTagsService) do |service|
+ expect(service).to receive(:execute).and_return(status: delete_tags_service_status)
end
+ end
- original_size = 7
- keep_n = 1
+ original_size = 7
+ keep_n = 1
- it_behaves_like(
- 'returning the response',
- status: params[:expected_status],
- original_size: original_size,
- before_truncate_size: original_size - keep_n,
- after_truncate_size: params[:expected_truncated] ? params[:max_list_size] + keep_n : original_size - keep_n,
- before_delete_size: params[:expected_truncated] ? params[:max_list_size] : original_size - keep_n - 1 # one tag is filtered out with older_than filter
- )
- end
+ it_behaves_like(
+ 'returning the response',
+ status: params[:expected_status],
+ original_size: original_size,
+ before_truncate_size: original_size - keep_n,
+ after_truncate_size: params[:expected_truncated] ? params[:max_list_size] + keep_n : original_size - keep_n,
+ before_delete_size: params[:expected_truncated] ? params[:max_list_size] : original_size - keep_n - 1 # one tag is filtered out with older_than filter
+ )
end
end
- context 'caching' do
+ context 'caching', :freeze_time do
let(:params) do
{
'name_regex_delete' => '.*',
@@ -381,17 +379,12 @@ RSpec.describe Projects::ContainerRepository::CleanupTagsService, :clean_gitlab_
before do
expect_delete(%w(Bb Ba C), container_expiration_policy: true)
- travel_to(Time.zone.local(2021, 9, 2, 12, 0, 0))
# We froze time so we need to set the created_at stubs again
stub_digest_config('sha256:configA', 1.hour.ago)
stub_digest_config('sha256:configB', 5.days.ago)
stub_digest_config('sha256:configC', 1.month.ago)
end
- after do
- travel_back
- end
-
it 'caches the created_at values' do
::Gitlab::Redis::Cache.with do |redis|
expect_mget(redis, tags_and_created_ats.keys)
@@ -450,32 +443,6 @@ RSpec.describe Projects::ContainerRepository::CleanupTagsService, :clean_gitlab_
DateTime.rfc3339(date_time.rfc3339).rfc3339
end
end
-
- context 'with container_registry_expiration_policies_caching enabled for the project' do
- before do
- stub_feature_flags(container_registry_expiration_policies_caching: project)
- end
-
- it_behaves_like 'reading and removing tags', caching_enabled: true
- end
-
- context 'with container_registry_expiration_policies_caching disabled' do
- before do
- stub_feature_flags(container_registry_expiration_policies_caching: false)
- end
-
- it_behaves_like 'reading and removing tags', caching_enabled: false
- end
-
- context 'with container_registry_expiration_policies_caching not enabled for the project' do
- let_it_be(:another_project) { create(:project) }
-
- before do
- stub_feature_flags(container_registry_expiration_policies_caching: another_project)
- end
-
- it_behaves_like 'reading and removing tags', caching_enabled: false
- end
end
private
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index d7c43ac676e..2aa9be5066f 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -49,6 +49,7 @@ RSpec.describe Projects::CreateService, '#execute' do
it 'keeps them as specified' do
expect(project.name).to eq('one')
expect(project.path).to eq('two')
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
end
@@ -58,6 +59,7 @@ RSpec.describe Projects::CreateService, '#execute' do
it 'sets name == path' do
expect(project.path).to eq('one.two_three-four')
expect(project.name).to eq(project.path)
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
end
@@ -67,6 +69,7 @@ RSpec.describe Projects::CreateService, '#execute' do
it 'sets path == name' do
expect(project.name).to eq('one.two_three-four')
expect(project.path).to eq(project.name)
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
end
@@ -78,6 +81,7 @@ RSpec.describe Projects::CreateService, '#execute' do
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')
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
end
end
@@ -111,13 +115,14 @@ RSpec.describe Projects::CreateService, '#execute' do
end
context 'user namespace' do
- it do
+ it 'creates a project in user namespace' do
project = create_project(user, opts)
expect(project).to be_valid
expect(project.owner).to eq(user)
expect(project.team.maintainers).to include(user)
expect(project.namespace).to eq(user.namespace)
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
end
@@ -151,6 +156,7 @@ RSpec.describe Projects::CreateService, '#execute' do
expect(project.owner).to eq(user)
expect(project.team.maintainers).to contain_exactly(user)
expect(project.namespace).to eq(user.namespace)
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
end
@@ -160,6 +166,7 @@ RSpec.describe Projects::CreateService, '#execute' do
project = create_project(admin, opts)
expect(project).not_to be_persisted
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
end
end
@@ -183,6 +190,7 @@ RSpec.describe Projects::CreateService, '#execute' do
expect(project.namespace).to eq(group)
expect(project.team.owners).to include(user)
expect(user.authorized_projects).to include(project)
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
end
@@ -339,6 +347,7 @@ RSpec.describe Projects::CreateService, '#execute' do
end
imported_project
+ expect(imported_project.project_namespace).to be_in_sync_with_project(imported_project)
end
it 'stores import data and URL' do
@@ -406,6 +415,7 @@ RSpec.describe Projects::CreateService, '#execute' do
expect(project.visibility_level).to eq(project_level)
expect(project).to be_saved
expect(project).to be_valid
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
end
end
@@ -424,6 +434,7 @@ RSpec.describe Projects::CreateService, '#execute' do
expect(project.errors.messages[:visibility_level].first).to(
match('restricted by your GitLab administrator')
)
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
it 'does not allow a restricted visibility level for admins when admin mode is disabled' do
@@ -493,6 +504,7 @@ RSpec.describe Projects::CreateService, '#execute' do
expect(project).to be_valid
expect(project.owner).to eq(user)
expect(project.namespace).to eq(user.namespace)
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
context 'when another repository already exists on disk' do
@@ -522,6 +534,7 @@ RSpec.describe Projects::CreateService, '#execute' do
expect(project).to respond_to(:errors)
expect(project.errors.messages).to have_key(:base)
expect(project.errors.messages[:base].first).to match('There is already a repository with that name on disk')
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
it 'does not allow to import project when path matches existing repository on disk' do
@@ -531,6 +544,7 @@ RSpec.describe Projects::CreateService, '#execute' do
expect(project).to respond_to(:errors)
expect(project.errors.messages).to have_key(:base)
expect(project.errors.messages[:base].first).to match('There is already a repository with that name on disk')
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
end
@@ -555,6 +569,7 @@ RSpec.describe Projects::CreateService, '#execute' do
expect(project).to respond_to(:errors)
expect(project.errors.messages).to have_key(:base)
expect(project.errors.messages[:base].first).to match('There is already a repository with that name on disk')
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
end
end
@@ -651,7 +666,7 @@ RSpec.describe Projects::CreateService, '#execute' do
end
context 'with an active group-level integration' do
- let!(:group_integration) { create(:prometheus_integration, group: group, project: nil, api_url: 'https://prometheus.group.com/') }
+ let!(:group_integration) { create(:prometheus_integration, :group, group: group, api_url: 'https://prometheus.group.com/') }
let!(:group) do
create(:group).tap do |group|
group.add_owner(user)
@@ -672,7 +687,7 @@ RSpec.describe Projects::CreateService, '#execute' do
end
context 'with an active subgroup' do
- let!(:subgroup_integration) { create(:prometheus_integration, group: subgroup, project: nil, api_url: 'https://prometheus.subgroup.com/') }
+ let!(:subgroup_integration) { create(:prometheus_integration, :group, group: subgroup, api_url: 'https://prometheus.subgroup.com/') }
let!(:subgroup) do
create(:group, parent: group).tap do |subgroup|
subgroup.add_owner(user)
@@ -810,11 +825,11 @@ RSpec.describe Projects::CreateService, '#execute' do
).to be_truthy
end
- it 'schedules authorization update for users with access to group' do
+ it 'schedules authorization update for users with access to group', :sidekiq_inline do
expect(AuthorizedProjectsWorker).not_to(
receive(:bulk_perform_async)
)
- expect(AuthorizedProjectUpdate::ProjectCreateWorker).to(
+ expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to(
receive(:perform_async).and_call_original
)
expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
@@ -825,7 +840,11 @@ RSpec.describe Projects::CreateService, '#execute' do
.and_call_original
)
- create_project(user, opts)
+ project = create_project(user, opts)
+
+ expect(
+ Ability.allowed?(other_user, :developer_access, project)
+ ).to be_truthy
end
end
@@ -866,6 +885,7 @@ RSpec.describe Projects::CreateService, '#execute' do
expect(project).to be_valid
expect(project.shared_runners_enabled).to eq(expected_result_for_project)
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
end
end
@@ -886,6 +906,7 @@ RSpec.describe Projects::CreateService, '#execute' do
expect(project).to be_valid
expect(project.shared_runners_enabled).to eq(expected_result_for_project)
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
end
end
@@ -903,6 +924,7 @@ RSpec.describe Projects::CreateService, '#execute' do
expect(project.persisted?).to eq(false)
expect(project).to be_invalid
expect(project.errors[:shared_runners_enabled]).to include('cannot be enabled because parent group does not allow it')
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
end
end
@@ -922,6 +944,7 @@ RSpec.describe Projects::CreateService, '#execute' do
expect(project).to be_valid
expect(project.shared_runners_enabled).to eq(expected_result)
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
end
end
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index 9bdd9800fcc..ac84614121a 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -331,6 +331,14 @@ RSpec.describe Projects::DestroyService, :aggregate_failures do
end
end
end
+
+ context 'for an archived project' do
+ before do
+ project.update!(archived: true)
+ end
+
+ it_behaves_like 'deleting the project with pipeline and build'
+ end
end
describe 'container registry' do
diff --git a/spec/services/projects/import_export/export_service_spec.rb b/spec/services/projects/import_export/export_service_spec.rb
index 111c1264777..6002aaf427a 100644
--- a/spec/services/projects/import_export/export_service_spec.rb
+++ b/spec/services/projects/import_export/export_service_spec.rb
@@ -4,7 +4,8 @@ require 'spec_helper'
RSpec.describe Projects::ImportExport::ExportService do
describe '#execute' do
- let!(:user) { create(:user) }
+ let_it_be(:user) { create(:user) }
+
let(:project) { create(:project) }
let(:shared) { project.import_export_shared }
let!(:after_export_strategy) { Gitlab::ImportExport::AfterExportStrategies::DownloadNotificationStrategy.new }
@@ -28,7 +29,14 @@ RSpec.describe Projects::ImportExport::ExportService do
end
it 'saves the models' do
- expect(Gitlab::ImportExport::Project::TreeSaver).to receive(:new).and_call_original
+ saver_params = {
+ project: project,
+ current_user: user,
+ shared: shared,
+ params: {},
+ logger: an_instance_of(Gitlab::Export::Logger)
+ }
+ expect(Gitlab::ImportExport::Project::TreeSaver).to receive(:new).with(saver_params).and_call_original
service.execute
end
diff --git a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
index f9ff959fa05..04c6349bf52 100644
--- a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
@@ -102,6 +102,7 @@ RSpec.describe Projects::LfsPointers::LfsDownloadService do
it 'skips read_total_timeout', :aggregate_failures do
stub_const('GitLab::HTTP::DEFAULT_READ_TOTAL_TIMEOUT', 0)
+ expect(ProjectCacheWorker).to receive(:perform_async).once
expect(Gitlab::Metrics::System).not_to receive(:monotonic_time)
expect(subject.execute).to include(status: :success)
end
diff --git a/spec/services/projects/participants_service_spec.rb b/spec/services/projects/participants_service_spec.rb
index eab7228307a..61edfd23700 100644
--- a/spec/services/projects/participants_service_spec.rb
+++ b/spec/services/projects/participants_service_spec.rb
@@ -207,13 +207,5 @@ RSpec.describe Projects::ParticipantsService do
end
it_behaves_like 'return project members'
-
- context 'when feature flag :linear_participants_service_ancestor_scopes is disabled' do
- before do
- stub_feature_flags(linear_participants_service_ancestor_scopes: false)
- end
-
- it_behaves_like 'return project members'
- end
end
end
diff --git a/spec/services/projects/prometheus/alerts/notify_service_spec.rb b/spec/services/projects/prometheus/alerts/notify_service_spec.rb
index 25cf588dedf..3bd96ad19bc 100644
--- a/spec/services/projects/prometheus/alerts/notify_service_spec.rb
+++ b/spec/services/projects/prometheus/alerts/notify_service_spec.rb
@@ -218,8 +218,7 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do
.to receive(:new)
.with(project, kind_of(Hash))
.exactly(3).times
- .and_return(process_service)
- expect(process_service).to receive(:execute).exactly(3).times
+ .and_call_original
subject
end
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index b539b01066e..c47d44002cc 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Projects::TransferService do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
- let_it_be(:group_integration) { create(:integrations_slack, group: group, project: nil, webhook: 'http://group.slack.com') }
+ let_it_be(:group_integration) { create(:integrations_slack, :group, group: group, webhook: 'http://group.slack.com') }
let(:project) { create(:project, :repository, :legacy_storage, namespace: user.namespace) }
@@ -66,8 +66,6 @@ RSpec.describe Projects::TransferService do
end
context 'when project has an associated project namespace' do
- let!(:project_namespace) { create(:project_namespace, project: project) }
-
it 'keeps project namespace in sync with project' do
transfer_result = execute_transfer
@@ -272,8 +270,6 @@ RSpec.describe Projects::TransferService do
end
context 'when project has an associated project namespace' do
- let!(:project_namespace) { create(:project_namespace, project: project) }
-
it 'keeps project namespace in sync with project' do
attempt_project_transfer
@@ -294,8 +290,6 @@ RSpec.describe Projects::TransferService do
end
context 'when project has an associated project namespace' do
- let!(:project_namespace) { create(:project_namespace, project: project) }
-
it 'keeps project namespace in sync with project' do
transfer_result = execute_transfer
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index d67b189f90e..611261cd92c 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -1326,14 +1326,25 @@ RSpec.describe QuickActions::InterpretService do
let(:issuable) { issue }
end
- it_behaves_like 'confidential command' do
- let(:content) { '/confidential' }
- let(:issuable) { issue }
- end
+ context '/confidential' do
+ it_behaves_like 'confidential command' do
+ let(:content) { '/confidential' }
+ let(:issuable) { issue }
+ end
- it_behaves_like 'confidential command' do
- let(:content) { '/confidential' }
- let(:issuable) { create(:incident, project: project) }
+ it_behaves_like 'confidential command' do
+ let(:content) { '/confidential' }
+ let(:issuable) { create(:incident, project: project) }
+ end
+
+ context 'when non-member is creating a new issue' do
+ let(:service) { described_class.new(project, create(:user)) }
+
+ it_behaves_like 'confidential command' do
+ let(:content) { '/confidential' }
+ let(:issuable) { build(:issue, project: project) }
+ end
+ end
end
it_behaves_like 'lock command' do
@@ -2542,4 +2553,32 @@ RSpec.describe QuickActions::InterpretService do
end
end
end
+
+ describe '#available_commands' do
+ context 'when Guest is creating a new issue' do
+ let_it_be(:guest) { create(:user) }
+
+ let(:issue) { build(:issue, project: public_project) }
+ let(:service) { described_class.new(project, guest) }
+
+ before_all do
+ public_project.add_guest(guest)
+ end
+
+ it 'includes commands to set metadata' do
+ # milestone action is only available when project has a milestone
+ milestone
+
+ available_commands = service.available_commands(issue)
+
+ expect(available_commands).to include(
+ a_hash_including(name: :label),
+ a_hash_including(name: :milestone),
+ a_hash_including(name: :copy_metadata),
+ a_hash_including(name: :assign),
+ a_hash_including(name: :due)
+ )
+ end
+ end
+ end
end
diff --git a/spec/services/resource_events/change_labels_service_spec.rb b/spec/services/resource_events/change_labels_service_spec.rb
index b987e3204ad..c2c0a4c2126 100644
--- a/spec/services/resource_events/change_labels_service_spec.rb
+++ b/spec/services/resource_events/change_labels_service_spec.rb
@@ -54,7 +54,7 @@ RSpec.describe ResourceEvents::ChangeLabelsService do
let(:removed) { [labels[1]] }
it 'creates all label events in a single query' do
- expect(Gitlab::Database.main).to receive(:bulk_insert).once.and_call_original
+ expect(ApplicationRecord).to receive(:legacy_bulk_insert).once.and_call_original
expect { subject }.to change { resource.resource_label_events.count }.from(0).to(2)
end
end
diff --git a/spec/services/resource_events/synthetic_label_notes_builder_service_spec.rb b/spec/services/resource_events/synthetic_label_notes_builder_service_spec.rb
index cb42ad5b617..71b1d0993ee 100644
--- a/spec/services/resource_events/synthetic_label_notes_builder_service_spec.rb
+++ b/spec/services/resource_events/synthetic_label_notes_builder_service_spec.rb
@@ -4,18 +4,20 @@ require 'spec_helper'
RSpec.describe ResourceEvents::SyntheticLabelNotesBuilderService do
describe '#execute' do
- let!(:user) { create(:user) }
+ let_it_be(:user) { create(:user) }
- let!(:issue) { create(:issue, author: user) }
+ let_it_be(:issue) { create(:issue, author: user) }
- let!(:event1) { create(:resource_label_event, issue: issue) }
- let!(:event2) { create(:resource_label_event, issue: issue) }
- let!(:event3) { create(:resource_label_event, issue: issue) }
+ let_it_be(:event1) { create(:resource_label_event, issue: issue) }
+ let_it_be(:event2) { create(:resource_label_event, issue: issue) }
+ let_it_be(:event3) { create(:resource_label_event, issue: issue) }
it 'returns the expected synthetic notes' do
notes = ResourceEvents::SyntheticLabelNotesBuilderService.new(issue, user).execute
expect(notes.size).to eq(3)
end
+
+ it_behaves_like 'filters by paginated notes', :resource_label_event
end
end
diff --git a/spec/services/resource_events/synthetic_milestone_notes_builder_service_spec.rb b/spec/services/resource_events/synthetic_milestone_notes_builder_service_spec.rb
index 1b35e224e98..9c6b6a33b57 100644
--- a/spec/services/resource_events/synthetic_milestone_notes_builder_service_spec.rb
+++ b/spec/services/resource_events/synthetic_milestone_notes_builder_service_spec.rb
@@ -24,5 +24,7 @@ RSpec.describe ResourceEvents::SyntheticMilestoneNotesBuilderService do
'removed milestone'
])
end
+
+ it_behaves_like 'filters by paginated notes', :resource_milestone_event
end
end
diff --git a/spec/services/resource_events/synthetic_state_notes_builder_service_spec.rb b/spec/services/resource_events/synthetic_state_notes_builder_service_spec.rb
new file mode 100644
index 00000000000..79500f3768b
--- /dev/null
+++ b/spec/services/resource_events/synthetic_state_notes_builder_service_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ResourceEvents::SyntheticStateNotesBuilderService do
+ describe '#execute' do
+ let_it_be(:user) { create(:user) }
+
+ it_behaves_like 'filters by paginated notes', :resource_state_event
+ end
+end
diff --git a/spec/services/security/ci_configuration/sast_iac_create_service_spec.rb b/spec/services/security/ci_configuration/sast_iac_create_service_spec.rb
new file mode 100644
index 00000000000..deb10732b37
--- /dev/null
+++ b/spec/services/security/ci_configuration/sast_iac_create_service_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Security::CiConfiguration::SastIacCreateService, :snowplow do
+ subject(:result) { described_class.new(project, user).execute }
+
+ let(:branch_name) { 'set-sast-iac-config-1' }
+
+ let(:snowplow_event) do
+ {
+ category: 'Security::CiConfiguration::SastIacCreateService',
+ action: 'create',
+ label: ''
+ }
+ end
+
+ include_examples 'services security ci configuration create service', true
+end
diff --git a/spec/services/spam/spam_verdict_service_spec.rb b/spec/services/spam/spam_verdict_service_spec.rb
index 659c21b7d4f..99047f3233b 100644
--- a/spec/services/spam/spam_verdict_service_spec.rb
+++ b/spec/services/spam/spam_verdict_service_spec.rb
@@ -267,8 +267,8 @@ RSpec.describe Spam::SpamVerdictService do
where(:verdict_value, :expected) do
::Spam::SpamConstants::ALLOW | ::Spam::SpamConstants::ALLOW
::Spam::SpamConstants::CONDITIONAL_ALLOW | ::Spam::SpamConstants::CONDITIONAL_ALLOW
- ::Spam::SpamConstants::DISALLOW | ::Spam::SpamConstants::CONDITIONAL_ALLOW
- ::Spam::SpamConstants::BLOCK_USER | ::Spam::SpamConstants::CONDITIONAL_ALLOW
+ ::Spam::SpamConstants::DISALLOW | ::Spam::SpamConstants::DISALLOW
+ ::Spam::SpamConstants::BLOCK_USER | ::Spam::SpamConstants::BLOCK_USER
end
# rubocop: enable Lint/BinaryOperatorWithIdenticalOperands
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 1a421999ffb..ce0122ae301 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -348,193 +348,6 @@ RSpec.describe SystemNoteService do
end
end
- describe 'Jira integration' do
- include JiraServiceHelper
-
- let(:project) { create(:jira_project, :repository) }
- let(:author) { create(:user) }
- let(:issue) { create(:issue, project: project) }
- let(:merge_request) { create(:merge_request, :simple, target_project: project, source_project: project) }
- let(:jira_issue) { ExternalIssue.new("JIRA-1", project)}
- let(:jira_tracker) { project.jira_integration }
- let(:commit) { project.commit }
- let(:comment_url) { jira_api_comment_url(jira_issue.id) }
- let(:success_message) { "SUCCESS: Successfully posted to http://jira.example.net." }
-
- before do
- stub_jira_integration_test
- stub_jira_urls(jira_issue.id)
- jira_integration_settings
- end
-
- def cross_reference(type, link_exists = false)
- noteable = type == 'commit' ? commit : merge_request
-
- links = []
- if link_exists
- url = if type == 'commit'
- "#{Settings.gitlab.base_url}/#{project.namespace.path}/#{project.path}/-/commit/#{commit.id}"
- else
- "#{Settings.gitlab.base_url}/#{project.namespace.path}/#{project.path}/-/merge_requests/#{merge_request.iid}"
- end
-
- link = double(object: { 'url' => url })
- links << link
- expect(link).to receive(:save!)
- end
-
- allow(JIRA::Resource::Remotelink).to receive(:all).and_return(links)
-
- described_class.cross_reference(jira_issue, noteable, author)
- end
-
- noteable_types = %w(merge_requests commit)
-
- noteable_types.each do |type|
- context "when noteable is a #{type}" do
- it "blocks cross reference when #{type.underscore}_events is false" do
- jira_tracker.update!("#{type}_events" => false)
-
- expect(cross_reference(type)).to eq(s_('JiraService|Events for %{noteable_model_name} are disabled.') % { noteable_model_name: type.pluralize.humanize.downcase })
- end
-
- it "creates cross reference when #{type.underscore}_events is true" do
- jira_tracker.update!("#{type}_events" => true)
-
- expect(cross_reference(type)).to eq(success_message)
- end
- end
-
- context 'when a new cross reference is created' do
- it 'creates a new comment and remote link' do
- cross_reference(type)
-
- expect(WebMock).to have_requested(:post, jira_api_comment_url(jira_issue))
- expect(WebMock).to have_requested(:post, jira_api_remote_link_url(jira_issue))
- end
- end
-
- context 'when a link exists' do
- it 'updates a link but does not create a new comment' do
- expect(WebMock).not_to have_requested(:post, jira_api_comment_url(jira_issue))
-
- cross_reference(type, true)
- end
- end
- end
-
- describe "new reference" do
- let(:favicon_path) { "http://localhost/assets/#{find_asset('favicon.png').digest_path}" }
-
- before do
- allow(JIRA::Resource::Remotelink).to receive(:all).and_return([])
- end
-
- context 'for commits' do
- it "creates comment" do
- result = described_class.cross_reference(jira_issue, commit, author)
-
- expect(result).to eq(success_message)
- end
-
- it "creates remote link" do
- described_class.cross_reference(jira_issue, commit, author)
-
- expect(WebMock).to have_requested(:post, jira_api_remote_link_url(jira_issue)).with(
- body: hash_including(
- GlobalID: "GitLab",
- relationship: 'mentioned on',
- object: {
- url: project_commit_url(project, commit),
- title: "Commit - #{commit.title}",
- icon: { title: "GitLab", url16x16: favicon_path },
- status: { resolved: false }
- }
- )
- ).once
- end
- end
-
- context 'for issues' do
- let(:issue) { create(:issue, project: project) }
-
- it "creates comment" do
- result = described_class.cross_reference(jira_issue, issue, author)
-
- expect(result).to eq(success_message)
- end
-
- it "creates remote link" do
- described_class.cross_reference(jira_issue, issue, author)
-
- expect(WebMock).to have_requested(:post, jira_api_remote_link_url(jira_issue)).with(
- body: hash_including(
- GlobalID: "GitLab",
- relationship: 'mentioned on',
- object: {
- url: project_issue_url(project, issue),
- title: "Issue - #{issue.title}",
- icon: { title: "GitLab", url16x16: favicon_path },
- status: { resolved: false }
- }
- )
- ).once
- end
- end
-
- context 'for snippets' do
- let(:snippet) { create(:snippet, project: project) }
-
- it "creates comment" do
- result = described_class.cross_reference(jira_issue, snippet, author)
-
- expect(result).to eq(success_message)
- end
-
- it "creates remote link" do
- described_class.cross_reference(jira_issue, snippet, author)
-
- expect(WebMock).to have_requested(:post, jira_api_remote_link_url(jira_issue)).with(
- body: hash_including(
- GlobalID: "GitLab",
- relationship: 'mentioned on',
- object: {
- url: project_snippet_url(project, snippet),
- title: "Snippet - #{snippet.title}",
- icon: { title: "GitLab", url16x16: favicon_path },
- status: { resolved: false }
- }
- )
- ).once
- end
- end
- end
-
- describe "existing reference" do
- before do
- allow(JIRA::Resource::Remotelink).to receive(:all).and_return([])
- message = double('message')
- allow(message).to receive(:include?) { true }
- allow_next_instance_of(JIRA::Resource::Issue) do |instance|
- allow(instance).to receive(:comments).and_return([OpenStruct.new(body: message)])
- end
- end
-
- it "does not return success message" do
- result = described_class.cross_reference(jira_issue, commit, author)
-
- expect(result).not_to eq(success_message)
- end
-
- it 'does not try to create comment and remote link' do
- subject
-
- expect(WebMock).not_to have_requested(:post, jira_api_comment_url(jira_issue))
- expect(WebMock).not_to have_requested(:post, jira_api_remote_link_url(jira_issue))
- end
- end
- end
-
describe '.change_time_estimate' do
it 'calls TimeTrackingService' do
expect_next_instance_of(::SystemNotes::TimeTrackingService) do |service|
@@ -781,6 +594,18 @@ RSpec.describe SystemNoteService do
end
end
+ describe '.resolve_incident_status' do
+ let(:incident) { build(:incident, :closed) }
+
+ it 'calls IncidentService' do
+ expect_next_instance_of(SystemNotes::IncidentService) do |service|
+ expect(service).to receive(:resolve_incident_status)
+ end
+
+ described_class.resolve_incident_status(incident, author)
+ end
+ end
+
describe '.log_resolving_alert' do
let(:alert) { build(:alert_management_alert) }
let(:monitoring_tool) { 'Prometheus' }
diff --git a/spec/services/system_notes/incident_service_spec.rb b/spec/services/system_notes/incident_service_spec.rb
index ab9b9eb2bd4..669e357b7a4 100644
--- a/spec/services/system_notes/incident_service_spec.rb
+++ b/spec/services/system_notes/incident_service_spec.rb
@@ -56,4 +56,14 @@ RSpec.describe ::SystemNotes::IncidentService do
end
end
end
+
+ describe '#resolve_incident_status' do
+ subject(:resolve_incident_status) { described_class.new(noteable: noteable, project: project, author: author).resolve_incident_status }
+
+ it 'creates a new note about resolved incident', :aggregate_failures do
+ expect { resolve_incident_status }.to change { noteable.notes.count }.by(1)
+
+ expect(noteable.notes.last.note).to eq('changed the status to **Resolved** by closing the incident')
+ end
+ end
end
diff --git a/spec/services/system_notes/issuables_service_spec.rb b/spec/services/system_notes/issuables_service_spec.rb
index 71a28a89cd8..fd481aa6ddb 100644
--- a/spec/services/system_notes/issuables_service_spec.rb
+++ b/spec/services/system_notes/issuables_service_spec.rb
@@ -347,6 +347,23 @@ RSpec.describe ::SystemNotes::IssuablesService do
end
end
end
+
+ context 'with external issue' do
+ let(:noteable) { ExternalIssue.new('JIRA-123', project) }
+ let(:mentioner) { project.commit }
+
+ it 'queues a background worker' do
+ expect(Integrations::CreateExternalCrossReferenceWorker).to receive(:perform_async).with(
+ project.id,
+ 'JIRA-123',
+ 'Commit',
+ mentioner.id,
+ author.id
+ )
+
+ subject
+ end
+ end
end
end
diff --git a/spec/services/tasks_to_be_done/base_service_spec.rb b/spec/services/tasks_to_be_done/base_service_spec.rb
new file mode 100644
index 00000000000..bf6be6d46e5
--- /dev/null
+++ b/spec/services/tasks_to_be_done/base_service_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe TasksToBeDone::BaseService do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:assignee_one) { create(:user) }
+ let_it_be(:assignee_two) { create(:user) }
+ let_it_be(:assignee_ids) { [assignee_one.id] }
+ let_it_be(:label) { create(:label, title: 'tasks to be done:ci', project: project) }
+
+ before do
+ project.add_maintainer(current_user)
+ project.add_developer(assignee_one)
+ project.add_developer(assignee_two)
+ end
+
+ subject(:service) do
+ TasksToBeDone::CreateCiTaskService.new(
+ project: project,
+ current_user: current_user,
+ assignee_ids: assignee_ids
+ )
+ end
+
+ context 'no existing task issue', :aggregate_failures do
+ it 'creates an issue' do
+ params = {
+ assignee_ids: assignee_ids,
+ title: 'Set up CI/CD',
+ description: anything,
+ add_labels: label.title
+ }
+
+ expect(Issues::BuildService)
+ .to receive(:new)
+ .with(project: project, current_user: current_user, params: params)
+ .and_call_original
+
+ expect { service.execute }.to change(Issue, :count).by(1)
+
+ expect(project.issues.last).to have_attributes(
+ author: current_user,
+ title: params[:title],
+ assignees: [assignee_one],
+ labels: [label]
+ )
+ end
+ end
+
+ context 'an open issue with the same label already exists', :aggregate_failures do
+ let_it_be(:assignee_ids) { [assignee_two.id] }
+
+ it 'assigns the user to the existing issue' do
+ issue = create(:labeled_issue, project: project, labels: [label], assignees: [assignee_one])
+ params = { add_assignee_ids: assignee_ids }
+
+ expect(Issues::UpdateService)
+ .to receive(:new)
+ .with(project: project, current_user: current_user, params: params)
+ .and_call_original
+
+ expect { service.execute }.not_to change(Issue, :count)
+
+ expect(issue.reload.assignees).to match_array([assignee_one, assignee_two])
+ end
+ end
+end
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 6a8e6dc8970..7103cb0b66a 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -1218,6 +1218,17 @@ RSpec.describe TodoService do
end
end
+ describe '#create_attention_requested_todo' do
+ let(:target) { create(:merge_request, author: author, source_project: project) }
+ let(:user) { create(:user) }
+
+ it 'creates a todo for user' do
+ service.create_attention_requested_todo(target, author, user)
+
+ should_create_todo(user: user, target: target, action: Todo::ATTENTION_REQUESTED)
+ end
+ end
+
def should_create_todo(attributes = {})
attributes.reverse_merge!(
project: project,
diff --git a/spec/services/users/update_service_spec.rb b/spec/services/users/update_service_spec.rb
index 3244db4c1fb..52c7b54ed72 100644
--- a/spec/services/users/update_service_spec.rb
+++ b/spec/services/users/update_service_spec.rb
@@ -53,7 +53,7 @@ RSpec.describe Users::UpdateService do
result = update_user(user, status: { emoji: "Moo!" })
expect(result[:status]).to eq(:error)
- expect(result[:message]).to eq("Emoji is not included in the list")
+ expect(result[:message]).to eq("Emoji is not a valid emoji name")
end
it 'updates user detail with provided attributes' do
diff --git a/spec/services/users/upsert_credit_card_validation_service_spec.rb b/spec/services/users/upsert_credit_card_validation_service_spec.rb
index bede30e1898..952d482f1bd 100644
--- a/spec/services/users/upsert_credit_card_validation_service_spec.rb
+++ b/spec/services/users/upsert_credit_card_validation_service_spec.rb
@@ -15,6 +15,7 @@ RSpec.describe Users::UpsertCreditCardValidationService do
credit_card_expiration_year: expiration_year,
credit_card_expiration_month: 1,
credit_card_holder_name: 'John Smith',
+ credit_card_type: 'AmericanExpress',
credit_card_mask_number: '1111'
}
end
@@ -30,7 +31,16 @@ RSpec.describe Users::UpsertCreditCardValidationService do
result = service.execute
expect(result.status).to eq(:success)
- expect(user.reload.credit_card_validated_at).to eq(credit_card_validated_time)
+
+ user.reload
+
+ expect(user.credit_card_validation).to have_attributes(
+ credit_card_validated_at: credit_card_validated_time,
+ network: 'AmericanExpress',
+ holder_name: 'John Smith',
+ last_digits: 1111,
+ expiration_date: Date.new(expiration_year, 1, 31)
+ )
end
end
@@ -97,6 +107,7 @@ RSpec.describe Users::UpsertCreditCardValidationService do
expiration_date: Date.new(expiration_year, 1, 31),
holder_name: "John Smith",
last_digits: 1111,
+ network: "AmericanExpress",
user_id: user_id
}
diff --git a/spec/lib/gitlab/sidekiq_cluster_spec.rb b/spec/sidekiq_cluster/sidekiq_cluster_spec.rb
index 3c6ea054968..1d2b47e78ce 100644
--- a/spec/lib/gitlab/sidekiq_cluster_spec.rb
+++ b/spec/sidekiq_cluster/sidekiq_cluster_spec.rb
@@ -1,9 +1,10 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
require 'rspec-parameterized'
-RSpec.describe Gitlab::SidekiqCluster do
+require_relative '../../sidekiq_cluster/sidekiq_cluster'
+
+RSpec.describe Gitlab::SidekiqCluster do # rubocop:disable RSpec/FilePath
describe '.trap_signals' do
it 'traps the given signals' do
expect(described_class).to receive(:trap).ordered.with(:INT)
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index c8664598691..25759ca50b8 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -107,9 +107,7 @@ RSpec.configure do |config|
warn `curl -s -o log/goroutines.log http://localhost:9236/debug/pprof/goroutine?debug=2`
end
end
- end
-
- unless ENV['CI']
+ else
# Allow running `:focus` examples locally,
# falling back to all tests when there is no `:focus` example.
config.filter_run focus: true
@@ -199,6 +197,14 @@ 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
+
+ # Do not retry controller tests because rspec-retry cannot properly
+ # reset the controller which may contain data from last attempt. See
+ # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73360
+ config.prepend_before(:each, type: :controller) do |example|
+ example.metadata[:retry] = 1
+ end
+
config.exceptions_to_hard_fail = [DeprecationToolkitEnv::DeprecationBehaviors::SelectiveRaise::RaiseDisallowedDeprecation]
end
@@ -232,7 +238,7 @@ RSpec.configure do |config|
# We can't use an `around` hook here because the wrapping transaction
# is not yet opened at the time that is triggered
config.prepend_before do
- Gitlab::Database.main.set_open_transactions_baseline
+ ApplicationRecord.set_open_transactions_baseline
end
config.append_before do
@@ -240,7 +246,7 @@ RSpec.configure do |config|
end
config.append_after do
- Gitlab::Database.main.reset_open_transactions_baseline
+ ApplicationRecord.reset_open_transactions_baseline
end
config.before do |example|
@@ -431,6 +437,10 @@ RSpec.configure do |config|
Gitlab::Metrics.reset_registry!
end
+ config.before(:example, :eager_load) do
+ Rails.application.eager_load!
+ end
+
# This makes sure the `ApplicationController#can?` method is stubbed with the
# original implementation for all view specs.
config.before(:each, type: :view) do
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index ac35662ec93..14ef0f1b7e0 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -28,6 +28,8 @@ JS_CONSOLE_FILTER = Regexp.union([
CAPYBARA_WINDOW_SIZE = [1366, 768].freeze
+SCREENSHOT_FILENAME_LENGTH = ENV['CI'] || ENV['CI_SERVER'] ? 255 : 99
+
# Run Workhorse on the given host and port, proxying to Puma on a UNIX socket,
# for a closer-to-production experience
Capybara.register_server :puma_via_workhorse do |app, port, host, **options|
@@ -113,7 +115,7 @@ Capybara.enable_aria_label = true
Capybara::Screenshot.append_timestamp = false
Capybara::Screenshot.register_filename_prefix_formatter(:rspec) do |example|
- example.full_description.downcase.parameterize(separator: "_")[0..99]
+ example.full_description.downcase.parameterize(separator: "_")[0..SCREENSHOT_FILENAME_LENGTH]
end
# Keep only the screenshots generated from the last failing test suite
Capybara::Screenshot.prune_strategy = :keep_last_run
diff --git a/spec/support/database/cross-database-modification-allowlist.yml b/spec/support/database/cross-database-modification-allowlist.yml
index 627967f65f3..d05812a64eb 100644
--- a/spec/support/database/cross-database-modification-allowlist.yml
+++ b/spec/support/database/cross-database-modification-allowlist.yml
@@ -1,1343 +1,90 @@
-- "./ee/spec/controllers/admin/geo/nodes_controller_spec.rb"
-- "./ee/spec/controllers/admin/geo/projects_controller_spec.rb"
-- "./ee/spec/controllers/admin/projects_controller_spec.rb"
-- "./ee/spec/controllers/concerns/internal_redirect_spec.rb"
-- "./ee/spec/controllers/ee/projects/jobs_controller_spec.rb"
-- "./ee/spec/controllers/oauth/geo_auth_controller_spec.rb"
-- "./ee/spec/controllers/projects/approver_groups_controller_spec.rb"
-- "./ee/spec/controllers/projects/approvers_controller_spec.rb"
-- "./ee/spec/controllers/projects/merge_requests_controller_spec.rb"
-- "./ee/spec/controllers/projects/merge_requests/creations_controller_spec.rb"
- "./ee/spec/controllers/projects/settings/access_tokens_controller_spec.rb"
-- "./ee/spec/controllers/projects/subscriptions_controller_spec.rb"
-- "./ee/spec/features/account_recovery_regular_check_spec.rb"
-- "./ee/spec/features/admin/admin_audit_logs_spec.rb"
-- "./ee/spec/features/admin/admin_credentials_inventory_spec.rb"
-- "./ee/spec/features/admin/admin_dashboard_spec.rb"
-- "./ee/spec/features/admin/admin_dev_ops_report_spec.rb"
-- "./ee/spec/features/admin/admin_merge_requests_approvals_spec.rb"
-- "./ee/spec/features/admin/admin_reset_pipeline_minutes_spec.rb"
-- "./ee/spec/features/admin/admin_sends_notification_spec.rb"
-- "./ee/spec/features/admin/admin_settings_spec.rb"
-- "./ee/spec/features/admin/admin_show_new_user_signups_cap_alert_spec.rb"
-- "./ee/spec/features/admin/admin_users_spec.rb"
-- "./ee/spec/features/admin/geo/admin_geo_nodes_spec.rb"
-- "./ee/spec/features/admin/geo/admin_geo_projects_spec.rb"
-- "./ee/spec/features/admin/geo/admin_geo_replication_nav_spec.rb"
-- "./ee/spec/features/admin/geo/admin_geo_sidebar_spec.rb"
-- "./ee/spec/features/admin/geo/admin_geo_uploads_spec.rb"
-- "./ee/spec/features/admin/groups/admin_changes_plan_spec.rb"
-- "./ee/spec/features/admin/licenses/admin_uploads_license_spec.rb"
-- "./ee/spec/features/admin/licenses/show_user_count_threshold_spec.rb"
-- "./ee/spec/features/admin/subscriptions/admin_views_subscription_spec.rb"
-- "./ee/spec/features/analytics/code_analytics_spec.rb"
-- "./ee/spec/features/billings/billing_plans_spec.rb"
-- "./ee/spec/features/billings/extend_reactivate_trial_spec.rb"
-- "./ee/spec/features/billings/qrtly_reconciliation_alert_spec.rb"
-- "./ee/spec/features/boards/boards_licensed_features_spec.rb"
-- "./ee/spec/features/boards/boards_spec.rb"
-- "./ee/spec/features/boards/group_boards/board_deletion_spec.rb"
-- "./ee/spec/features/boards/group_boards/multiple_boards_spec.rb"
-- "./ee/spec/features/boards/new_issue_spec.rb"
-- "./ee/spec/features/boards/scoped_issue_board_spec.rb"
-- "./ee/spec/features/boards/sidebar_spec.rb"
-- "./ee/spec/features/boards/swimlanes/epics_swimlanes_drag_drop_spec.rb"
-- "./ee/spec/features/boards/swimlanes/epics_swimlanes_filtering_spec.rb"
-- "./ee/spec/features/boards/swimlanes/epics_swimlanes_sidebar_labels_spec.rb"
-- "./ee/spec/features/boards/swimlanes/epics_swimlanes_sidebar_spec.rb"
-- "./ee/spec/features/boards/swimlanes/epics_swimlanes_spec.rb"
-- "./ee/spec/features/boards/user_adds_lists_to_board_spec.rb"
-- "./ee/spec/features/boards/user_visits_board_spec.rb"
-- "./ee/spec/features/burndown_charts_spec.rb"
-- "./ee/spec/features/burnup_charts_spec.rb"
-- "./ee/spec/features/ci/ci_minutes_spec.rb"
-- "./ee/spec/features/ci_shared_runner_warnings_spec.rb"
-- "./ee/spec/features/clusters/create_agent_spec.rb"
-- "./ee/spec/features/dashboards/activity_spec.rb"
-- "./ee/spec/features/dashboards/groups_spec.rb"
-- "./ee/spec/features/dashboards/issues_spec.rb"
-- "./ee/spec/features/dashboards/merge_requests_spec.rb"
-- "./ee/spec/features/dashboards/operations_spec.rb"
-- "./ee/spec/features/dashboards/projects_spec.rb"
-- "./ee/spec/features/dashboards/todos_spec.rb"
-- "./ee/spec/features/discussion_comments/epic_quick_actions_spec.rb"
-- "./ee/spec/features/discussion_comments/epic_spec.rb"
-- "./ee/spec/features/epic_boards/epic_boards_sidebar_spec.rb"
-- "./ee/spec/features/epic_boards/epic_boards_spec.rb"
-- "./ee/spec/features/epic_boards/multiple_epic_boards_spec.rb"
-- "./ee/spec/features/epic_boards/new_epic_spec.rb"
-- "./ee/spec/features/epics/delete_epic_spec.rb"
-- "./ee/spec/features/epics/epic_issues_spec.rb"
-- "./ee/spec/features/epics/epic_labels_spec.rb"
-- "./ee/spec/features/epics/epic_show_spec.rb"
-- "./ee/spec/features/epics/epics_list_spec.rb"
-- "./ee/spec/features/epics/filtered_search/visual_tokens_spec.rb"
-- "./ee/spec/features/epics/gfm_autocomplete_spec.rb"
-- "./ee/spec/features/epics/issue_promotion_spec.rb"
-- "./ee/spec/features/epics/referencing_epics_spec.rb"
-- "./ee/spec/features/epics/shortcuts_epic_spec.rb"
-- "./ee/spec/features/epics/todo_spec.rb"
-- "./ee/spec/features/epics/update_epic_spec.rb"
-- "./ee/spec/features/epics/user_uses_quick_actions_spec.rb"
-- "./ee/spec/features/geo_node_spec.rb"
-- "./ee/spec/features/groups/analytics/ci_cd_analytics_spec.rb"
-- "./ee/spec/features/groups/analytics/cycle_analytics/charts_spec.rb"
-- "./ee/spec/features/groups/analytics/cycle_analytics/filters_and_data_spec.rb"
-- "./ee/spec/features/groups/analytics/cycle_analytics/multiple_value_streams_spec.rb"
-- "./ee/spec/features/groups/audit_events_spec.rb"
-- "./ee/spec/features/groups/billing_spec.rb"
-- "./ee/spec/features/groups/contribution_analytics_spec.rb"
-- "./ee/spec/features/groups/group_overview_spec.rb"
-- "./ee/spec/features/groups/group_roadmap_spec.rb"
-- "./ee/spec/features/groups/group_settings_spec.rb"
-- "./ee/spec/features/groups/groups_security_credentials_spec.rb"
-- "./ee/spec/features/groups/hooks/user_tests_hooks_spec.rb"
-- "./ee/spec/features/groups/insights_spec.rb"
-- "./ee/spec/features/groups/issues_spec.rb"
-- "./ee/spec/features/groups/iterations/iterations_list_spec.rb"
-- "./ee/spec/features/groups/iteration_spec.rb"
-- "./ee/spec/features/groups/iterations/user_creates_iteration_in_cadence_spec.rb"
-- "./ee/spec/features/groups/iterations/user_edits_iteration_cadence_spec.rb"
-- "./ee/spec/features/groups/iterations/user_edits_iteration_spec.rb"
-- "./ee/spec/features/groups/iterations/user_views_iteration_cadence_spec.rb"
-- "./ee/spec/features/groups/iterations/user_views_iteration_spec.rb"
-- "./ee/spec/features/groups/ldap_group_links_spec.rb"
-- "./ee/spec/features/groups/ldap_settings_spec.rb"
-- "./ee/spec/features/groups/members/leave_group_spec.rb"
-- "./ee/spec/features/groups/members/list_members_spec.rb"
-- "./ee/spec/features/groups/members/override_ldap_memberships_spec.rb"
-- "./ee/spec/features/groups/new_spec.rb"
-- "./ee/spec/features/groups/push_rules_spec.rb"
-- "./ee/spec/features/groups/saml_providers_spec.rb"
-- "./ee/spec/features/groups/scim_token_spec.rb"
-- "./ee/spec/features/groups/seat_usage/seat_usage_spec.rb"
-- "./ee/spec/features/groups/security/compliance_dashboards_spec.rb"
-- "./ee/spec/features/groups/settings/user_configures_insights_spec.rb"
-- "./ee/spec/features/groups/settings/user_searches_in_settings_spec.rb"
-- "./ee/spec/features/groups/sso_spec.rb"
-- "./ee/spec/features/groups/wikis_spec.rb"
-- "./ee/spec/features/groups/wiki/user_views_wiki_empty_spec.rb"
-- "./ee/spec/features/ide/user_commits_changes_spec.rb"
-- "./ee/spec/features/ide/user_opens_ide_spec.rb"
-- "./ee/spec/features/integrations/jira/jira_issues_list_spec.rb"
-- "./ee/spec/features/issues/blocking_issues_spec.rb"
-- "./ee/spec/features/issues/epic_in_issue_sidebar_spec.rb"
-- "./ee/spec/features/issues/filtered_search/filter_issues_by_iteration_spec.rb"
-- "./ee/spec/features/issues/filtered_search/filter_issues_epic_spec.rb"
-- "./ee/spec/features/issues/filtered_search/filter_issues_weight_spec.rb"
-- "./ee/spec/features/issues/form_spec.rb"
-- "./ee/spec/features/issues/gfm_autocomplete_ee_spec.rb"
-- "./ee/spec/features/issues/issue_actions_spec.rb"
-- "./ee/spec/features/issues/issue_sidebar_spec.rb"
-- "./ee/spec/features/issues/move_issue_resource_weight_events_spec.rb"
-- "./ee/spec/features/issues/related_issues_spec.rb"
-- "./ee/spec/features/issues/resource_weight_events_spec.rb"
-- "./ee/spec/features/issues/user_bulk_edits_issues_spec.rb"
-- "./ee/spec/features/issues/user_edits_issue_spec.rb"
-- "./ee/spec/features/issues/user_uses_quick_actions_spec.rb"
-- "./ee/spec/features/issues/user_views_issues_spec.rb"
-- "./ee/spec/features/labels_hierarchy_spec.rb"
-- "./ee/spec/features/markdown/metrics_spec.rb"
-- "./ee/spec/features/merge_requests/user_filters_by_approvers_spec.rb"
-- "./ee/spec/features/merge_requests/user_resets_approvers_spec.rb"
-- "./ee/spec/features/merge_requests/user_views_all_merge_requests_spec.rb"
-- "./ee/spec/features/merge_request/user_approves_with_password_spec.rb"
-- "./ee/spec/features/merge_request/user_creates_merge_request_spec.rb"
-- "./ee/spec/features/merge_request/user_creates_merge_request_with_blocking_mrs_spec.rb"
-- "./ee/spec/features/merge_request/user_creates_multiple_assignees_mr_spec.rb"
-- "./ee/spec/features/merge_request/user_creates_multiple_reviewers_mr_spec.rb"
-- "./ee/spec/features/merge_request/user_edits_approval_rules_mr_spec.rb"
-- "./ee/spec/features/merge_request/user_edits_merge_request_blocking_mrs_spec.rb"
-- "./ee/spec/features/merge_request/user_edits_multiple_assignees_mr_spec.rb"
-- "./ee/spec/features/merge_request/user_edits_multiple_reviewers_mr_spec.rb"
-- "./ee/spec/features/merge_request/user_merges_immediately_spec.rb"
-- "./ee/spec/features/merge_request/user_merges_with_push_rules_spec.rb"
-- "./ee/spec/features/merge_request/user_sees_approval_widget_spec.rb"
-- "./ee/spec/features/merge_request/user_sees_closing_issues_message_spec.rb"
-- "./ee/spec/features/merge_request/user_sees_merge_widget_spec.rb"
-- "./ee/spec/features/merge_request/user_sees_status_checks_widget_spec.rb"
-- "./ee/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb"
-- "./ee/spec/features/merge_request/user_sets_approval_rules_spec.rb"
-- "./ee/spec/features/merge_request/user_sets_approvers_spec.rb"
-- "./ee/spec/features/merge_request/user_uses_slash_commands_spec.rb"
-- "./ee/spec/features/merge_request/user_views_blocked_merge_request_spec.rb"
-- "./ee/spec/features/merge_trains/user_adds_merge_request_to_merge_train_spec.rb"
-- "./ee/spec/features/merge_trains/user_adds_to_merge_train_when_pipeline_succeeds_spec.rb"
-- "./ee/spec/features/oncall_schedules/user_creates_schedule_spec.rb"
-- "./ee/spec/features/operations_nav_link_spec.rb"
-- "./ee/spec/features/profiles/account_spec.rb"
-- "./ee/spec/features/profiles/billing_spec.rb"
-- "./ee/spec/features/projects/audit_events_spec.rb"
-- "./ee/spec/features/projects/cluster_agents_spec.rb"
-- "./ee/spec/features/projects/custom_projects_template_spec.rb"
-- "./ee/spec/features/projects/environments/environments_spec.rb"
-- "./ee/spec/features/projects/feature_flags/feature_flag_issues_spec.rb"
-- "./ee/spec/features/projects/feature_flags/user_creates_feature_flag_spec.rb"
-- "./ee/spec/features/projects/feature_flags/user_deletes_feature_flag_spec.rb"
-- "./ee/spec/features/projects/feature_flags/user_sees_feature_flag_list_spec.rb"
-- "./ee/spec/features/projects/insights_spec.rb"
-- "./ee/spec/features/projects/integrations/user_activates_jira_spec.rb"
-- "./ee/spec/features/projects/issues/user_creates_issue_spec.rb"
-- "./ee/spec/features/projects/iterations/iteration_cadences_list_spec.rb"
-- "./ee/spec/features/projects/iterations/iterations_list_spec.rb"
-- "./ee/spec/features/projects/iterations/user_views_iteration_spec.rb"
-- "./ee/spec/features/projects/jobs_spec.rb"
-- "./ee/spec/features/projects/kerberos_clone_instructions_spec.rb"
-- "./ee/spec/features/projects/licenses/maintainer_views_policies_spec.rb"
-- "./ee/spec/features/projects/members/member_is_removed_from_project_spec.rb"
-- "./ee/spec/features/projects/merge_requests/user_approves_merge_request_spec.rb"
-- "./ee/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb"
-- "./ee/spec/features/projects/mirror_spec.rb"
-- "./ee/spec/features/projects/new_project_from_template_spec.rb"
-- "./ee/spec/features/projects/new_project_spec.rb"
-- "./ee/spec/features/projects/path_locks_spec.rb"
-- "./ee/spec/features/projects/pipelines/pipeline_spec.rb"
-- "./ee/spec/features/projects/push_rules_spec.rb"
-- "./ee/spec/features/projects/quality/test_case_create_spec.rb"
-- "./ee/spec/features/projects/quality/test_case_list_spec.rb"
-- "./ee/spec/features/projects/quality/test_case_show_spec.rb"
-- "./ee/spec/features/projects/releases/user_views_release_spec.rb"
-- "./ee/spec/features/projects/requirements_management/requirements_list_spec.rb"
-- "./ee/spec/features/projects/security/dast_scanner_profiles_spec.rb"
-- "./ee/spec/features/projects/security/dast_site_profiles_spec.rb"
-- "./ee/spec/features/projects/security/user_creates_on_demand_scan_spec.rb"
-- "./ee/spec/features/projects/security/user_views_security_configuration_spec.rb"
-- "./ee/spec/features/projects/services/prometheus_custom_metrics_spec.rb"
-- "./ee/spec/features/projects/services/user_activates_github_spec.rb"
-- "./ee/spec/features/projects/settings/disable_merge_trains_setting_spec.rb"
-- "./ee/spec/features/projects/settings/ee/repository_mirrors_settings_spec.rb"
-- "./ee/spec/features/projects/settings/ee/service_desk_setting_spec.rb"
-- "./ee/spec/features/projects/settings/issues_settings_spec.rb"
-- "./ee/spec/features/projects/settings/merge_request_approvals_settings_spec.rb"
-- "./ee/spec/features/projects/settings/merge_requests_settings_spec.rb"
-- "./ee/spec/features/projects/settings/pipeline_subscriptions_spec.rb"
-- "./ee/spec/features/projects/settings/protected_environments_spec.rb"
-- "./ee/spec/features/projects/settings/user_manages_merge_pipelines_spec.rb"
-- "./ee/spec/features/projects/settings/user_manages_merge_trains_spec.rb"
-- "./ee/spec/features/projects_spec.rb"
-- "./ee/spec/features/projects/user_applies_custom_file_template_spec.rb"
-- "./ee/spec/features/projects/view_blob_with_code_owners_spec.rb"
-- "./ee/spec/features/projects/wiki/user_views_wiki_empty_spec.rb"
-- "./ee/spec/features/promotion_spec.rb"
-- "./ee/spec/features/protected_branches_spec.rb"
-- "./ee/spec/features/protected_tags_spec.rb"
-- "./ee/spec/features/registrations/combined_registration_spec.rb"
-- "./ee/spec/features/registrations/trial_during_signup_flow_spec.rb"
-- "./ee/spec/features/registrations/user_sees_new_onboarding_flow_spec.rb"
-- "./ee/spec/features/registrations/welcome_spec.rb"
-- "./ee/spec/features/search/elastic/global_search_spec.rb"
-- "./ee/spec/features/search/elastic/group_search_spec.rb"
-- "./ee/spec/features/search/elastic/project_search_spec.rb"
-- "./ee/spec/features/search/elastic/snippet_search_spec.rb"
-- "./ee/spec/features/search/user_searches_for_epics_spec.rb"
-- "./ee/spec/features/subscriptions/groups/edit_spec.rb"
-- "./ee/spec/features/trial_registrations/signup_spec.rb"
-- "./ee/spec/features/trials/capture_lead_spec.rb"
-- "./ee/spec/features/trials/select_namespace_spec.rb"
-- "./ee/spec/features/trials/show_trial_banner_spec.rb"
-- "./ee/spec/features/users/login_spec.rb"
-- "./ee/spec/finders/geo/attachment_legacy_registry_finder_spec.rb"
-- "./ee/spec/finders/geo/container_repository_registry_finder_spec.rb"
-- "./ee/spec/finders/geo/lfs_object_registry_finder_spec.rb"
-- "./ee/spec/finders/geo/merge_request_diff_registry_finder_spec.rb"
-- "./ee/spec/finders/geo/package_file_registry_finder_spec.rb"
-- "./ee/spec/finders/geo/pages_deployment_registry_finder_spec.rb"
-- "./ee/spec/finders/geo/pipeline_artifact_registry_finder_spec.rb"
-- "./ee/spec/finders/geo/project_registry_finder_spec.rb"
-- "./ee/spec/finders/merge_requests/by_approvers_finder_spec.rb"
-- "./ee/spec/frontend/fixtures/analytics/value_streams.rb"
-- "./ee/spec/graphql/mutations/dast_on_demand_scans/create_spec.rb"
-- "./ee/spec/graphql/mutations/dast/profiles/create_spec.rb"
-- "./ee/spec/graphql/mutations/dast/profiles/run_spec.rb"
-- "./ee/spec/graphql/mutations/dast/profiles/update_spec.rb"
-- "./ee/spec/graphql/mutations/merge_requests/accept_spec.rb"
-- "./ee/spec/graphql/resolvers/geo/group_wiki_repository_registries_resolver_spec.rb"
-- "./ee/spec/graphql/resolvers/geo/lfs_object_registries_resolver_spec.rb"
-- "./ee/spec/graphql/resolvers/geo/merge_request_diff_registries_resolver_spec.rb"
-- "./ee/spec/graphql/resolvers/geo/package_file_registries_resolver_spec.rb"
-- "./ee/spec/graphql/resolvers/geo/pages_deployment_registries_resolver_spec.rb"
-- "./ee/spec/graphql/resolvers/geo/pipeline_artifact_registries_resolver_spec.rb"
-- "./ee/spec/graphql/resolvers/geo/snippet_repository_registries_resolver_spec.rb"
-- "./ee/spec/graphql/resolvers/geo/terraform_state_version_registries_resolver_spec.rb"
-- "./ee/spec/graphql/resolvers/geo/upload_registries_resolver_spec.rb"
-- "./ee/spec/helpers/application_helper_spec.rb"
-- "./ee/spec/helpers/ee/geo_helper_spec.rb"
-- "./ee/spec/lib/analytics/devops_adoption/snapshot_calculator_spec.rb"
-- "./ee/spec/lib/ee/gitlab/background_migration/backfill_iteration_cadence_id_for_boards_spec.rb"
-- "./ee/spec/lib/ee/gitlab/background_migration/backfill_version_data_from_gitaly_spec.rb"
-- "./ee/spec/lib/ee/gitlab/background_migration/create_security_setting_spec.rb"
-- "./ee/spec/lib/ee/gitlab/background_migration/fix_ruby_object_in_audit_events_spec.rb"
-- "./ee/spec/lib/ee/gitlab/background_migration/migrate_approver_to_approval_rules_check_progress_spec.rb"
-- "./ee/spec/lib/ee/gitlab/background_migration/migrate_approver_to_approval_rules_in_batch_spec.rb"
-- "./ee/spec/lib/ee/gitlab/background_migration/migrate_approver_to_approval_rules_spec.rb"
-- "./ee/spec/lib/ee/gitlab/background_migration/migrate_devops_segments_to_groups_spec.rb"
-- "./ee/spec/lib/ee/gitlab/background_migration/migrate_security_scans_spec.rb"
-- "./ee/spec/lib/ee/gitlab/background_migration/move_epic_issues_after_epics_spec.rb"
-- "./ee/spec/lib/ee/gitlab/background_migration/populate_any_approval_rule_for_merge_requests_spec.rb"
-- "./ee/spec/lib/ee/gitlab/background_migration/populate_any_approval_rule_for_projects_spec.rb"
-- "./ee/spec/lib/ee/gitlab/background_migration/populate_latest_pipeline_ids_spec.rb"
-- "./ee/spec/lib/ee/gitlab/background_migration/populate_namespace_statistics_spec.rb"
-- "./ee/spec/lib/ee/gitlab/background_migration/populate_resolved_on_default_branch_column_spec.rb"
-- "./ee/spec/lib/ee/gitlab/background_migration/populate_uuids_for_security_findings_spec.rb"
-- "./ee/spec/lib/ee/gitlab/background_migration/populate_vulnerability_feedback_pipeline_id_spec.rb"
-- "./ee/spec/lib/ee/gitlab/background_migration/populate_vulnerability_historical_statistics_spec.rb"
-- "./ee/spec/lib/ee/gitlab/background_migration/prune_orphaned_geo_events_spec.rb"
-- "./ee/spec/lib/ee/gitlab/background_migration/remove_duplicate_cs_findings_spec.rb"
-- "./ee/spec/lib/ee/gitlab/background_migration/remove_duplicated_cs_findings_without_vulnerability_id_spec.rb"
-- "./ee/spec/lib/ee/gitlab/background_migration/remove_inaccessible_epic_todos_spec.rb"
-- "./ee/spec/lib/ee/gitlab/background_migration/remove_undefined_occurrence_confidence_level_spec.rb"
-- "./ee/spec/lib/ee/gitlab/background_migration/remove_undefined_occurrence_severity_level_spec.rb"
-- "./ee/spec/lib/ee/gitlab/background_migration/remove_undefined_vulnerability_confidence_level_spec.rb"
-- "./ee/spec/lib/ee/gitlab/background_migration/remove_undefined_vulnerability_severity_level_spec.rb"
-- "./ee/spec/lib/ee/gitlab/background_migration/update_location_fingerprint_for_container_scanning_findings_spec.rb"
-- "./ee/spec/lib/ee/gitlab/background_migration/update_vulnerabilities_from_dismissal_feedback_spec.rb"
-- "./ee/spec/lib/ee/gitlab/background_migration/update_vulnerabilities_to_dismissed_spec.rb"
-- "./ee/spec/lib/ee/gitlab/background_migration/update_vulnerability_confidence_spec.rb"
-- "./ee/spec/lib/ee/gitlab/database/connection_spec.rb"
-- "./ee/spec/lib/ee/gitlab/database_spec.rb"
-- "./ee/spec/lib/ee/gitlab/middleware/read_only_spec.rb"
-- "./ee/spec/lib/ee/gitlab/usage_data_spec.rb"
-- "./ee/spec/lib/gitlab/background_migration/fix_orphan_promoted_issues_spec.rb"
-- "./ee/spec/lib/gitlab/background_migration/user_mentions/create_resource_user_mention_spec.rb"
- "./ee/spec/lib/gitlab/ci/templates/Jobs/dast_default_branch_gitlab_ci_yaml_spec.rb"
-- "./ee/spec/lib/gitlab/geo/base_request_spec.rb"
-- "./ee/spec/lib/gitlab/geo/database_tasks_spec.rb"
-- "./ee/spec/lib/gitlab/geo/event_gap_tracking_spec.rb"
-- "./ee/spec/lib/gitlab/geo/geo_tasks_spec.rb"
-- "./ee/spec/lib/gitlab/geo/jwt_request_decoder_spec.rb"
-- "./ee/spec/lib/gitlab/geo/log_cursor/events/design_repository_updated_event_spec.rb"
-- "./ee/spec/lib/gitlab/geo/log_cursor/events/job_artifact_deleted_event_spec.rb"
-- "./ee/spec/lib/gitlab/geo/log_cursor/events/repository_created_event_spec.rb"
-- "./ee/spec/lib/gitlab/geo/log_cursor/events/repository_updated_event_spec.rb"
-- "./ee/spec/lib/gitlab/geo/oauth/login_state_spec.rb"
-- "./ee/spec/lib/gitlab/geo/oauth/logout_token_spec.rb"
-- "./ee/spec/lib/gitlab/geo/oauth/session_spec.rb"
-- "./ee/spec/lib/gitlab/geo/registry_batcher_spec.rb"
-- "./ee/spec/lib/gitlab/geo/replicable_model_spec.rb"
-- "./ee/spec/lib/gitlab/geo/replication/blob_downloader_spec.rb"
-- "./ee/spec/lib/gitlab/geo/replication/file_transfer_spec.rb"
-- "./ee/spec/lib/gitlab/geo/replicator_spec.rb"
-- "./ee/spec/lib/gitlab/git_access_spec.rb"
-- "./ee/spec/lib/pseudonymizer/dumper_spec.rb"
-- "./ee/spec/lib/system_check/geo/geo_database_configured_check_spec.rb"
-- "./ee/spec/lib/system_check/geo/http_connection_check_spec.rb"
-- "./ee/spec/lib/system_check/rake_task/geo_task_spec.rb"
- "./ee/spec/mailers/notify_spec.rb"
-- "./ee/spec/migrations/20190926180443_schedule_epic_issues_after_epics_move_spec.rb"
-- "./ee/spec/migrations/add_non_null_constraint_for_escalation_rule_on_pending_alert_escalations_spec.rb"
-- "./ee/spec/migrations/add_unique_constraint_to_software_licenses_spec.rb"
-- "./ee/spec/migrations/backfill_namespace_statistics_with_wiki_size_spec.rb"
-- "./ee/spec/migrations/backfill_operations_feature_flags_iid_spec.rb"
-- "./ee/spec/migrations/backfill_software_licenses_spdx_identifiers_spec.rb"
-- "./ee/spec/migrations/backfill_version_author_and_created_at_spec.rb"
-- "./ee/spec/migrations/cleanup_deploy_access_levels_for_removed_groups_spec.rb"
-- "./ee/spec/migrations/create_elastic_reindexing_subtasks_spec.rb"
-- "./ee/spec/migrations/fix_any_approver_rule_for_projects_spec.rb"
-- "./ee/spec/migrations/migrate_design_notes_mentions_to_db_spec.rb"
-- "./ee/spec/migrations/migrate_epic_mentions_to_db_spec.rb"
-- "./ee/spec/migrations/migrate_epic_notes_mentions_to_db_spec.rb"
-- "./ee/spec/migrations/migrate_license_management_artifacts_to_license_scanning_spec.rb"
-- "./ee/spec/migrations/migrate_saml_identities_to_scim_identities_spec.rb"
-- "./ee/spec/migrations/migrate_scim_identities_to_saml_for_new_users_spec.rb"
-- "./ee/spec/migrations/migrate_vulnerability_dismissal_feedback_spec.rb"
-- "./ee/spec/migrations/migrate_vulnerability_dismissals_spec.rb"
-- "./ee/spec/migrations/nullify_feature_flag_plaintext_tokens_spec.rb"
-- "./ee/spec/migrations/populate_vulnerability_historical_statistics_for_year_spec.rb"
-- "./ee/spec/migrations/remove_creations_in_gitlab_subscription_histories_spec.rb"
-- "./ee/spec/migrations/remove_cycle_analytics_total_stage_data_spec.rb"
-- "./ee/spec/migrations/remove_duplicated_cs_findings_spec.rb"
-- "./ee/spec/migrations/remove_duplicated_cs_findings_without_vulnerability_id_spec.rb"
-- "./ee/spec/migrations/remove_schedule_and_status_null_constraints_from_pending_escalations_alert_spec.rb"
-- "./ee/spec/migrations/schedule_fix_orphan_promoted_issues_spec.rb"
-- "./ee/spec/migrations/schedule_fix_ruby_object_in_audit_events_spec.rb"
-- "./ee/spec/migrations/schedule_merge_request_any_approval_rule_migration_spec.rb"
-- "./ee/spec/migrations/schedule_populate_dismissed_state_for_vulnerabilities_spec.rb"
-- "./ee/spec/migrations/schedule_populate_resolved_on_default_branch_column_spec.rb"
-- "./ee/spec/migrations/schedule_populate_vulnerability_historical_statistics_spec.rb"
-- "./ee/spec/migrations/schedule_project_any_approval_rule_migration_spec.rb"
-- "./ee/spec/migrations/schedule_remove_inaccessible_epic_todos_spec.rb"
-- "./ee/spec/migrations/schedule_sync_blocking_issues_count_spec.rb"
-- "./ee/spec/migrations/schedule_uuid_population_for_security_findings2_spec.rb"
-- "./ee/spec/migrations/set_report_type_for_vulnerabilities_spec.rb"
-- "./ee/spec/migrations/set_resolved_state_on_vulnerabilities_spec.rb"
-- "./ee/spec/migrations/update_cs_vulnerability_confidence_column_spec.rb"
-- "./ee/spec/migrations/update_gitlab_subscriptions_start_at_post_eoa_spec.rb"
-- "./ee/spec/migrations/update_location_fingerprint_column_for_cs_spec.rb"
-- "./ee/spec/migrations/update_occurrence_severity_column_spec.rb"
-- "./ee/spec/migrations/update_undefined_confidence_from_occurrences_spec.rb"
-- "./ee/spec/migrations/update_undefined_confidence_from_vulnerabilities_spec.rb"
-- "./ee/spec/migrations/update_vulnerability_severity_column_spec.rb"
-- "./ee/spec/models/analytics/cycle_analytics/group_level_spec.rb"
-- "./ee/spec/models/approval_merge_request_rule_spec.rb"
-- "./ee/spec/models/approval_project_rule_spec.rb"
-- "./ee/spec/models/approval_state_spec.rb"
-- "./ee/spec/models/approval_wrapped_code_owner_rule_spec.rb"
-- "./ee/spec/models/approval_wrapped_rule_spec.rb"
-- "./ee/spec/models/approver_group_spec.rb"
- "./ee/spec/models/ci/bridge_spec.rb"
- "./ee/spec/models/ci/build_spec.rb"
- "./ee/spec/models/ci/minutes/additional_pack_spec.rb"
-- "./ee/spec/models/ci/pipeline_spec.rb"
-- "./ee/spec/models/ci/subscriptions/project_spec.rb"
-- "./ee/spec/models/concerns/approval_rule_like_spec.rb"
-- "./ee/spec/models/concerns/approver_migrate_hook_spec.rb"
-- "./ee/spec/models/dora/daily_metrics_spec.rb"
- "./ee/spec/models/ee/ci/job_artifact_spec.rb"
-- "./ee/spec/models/ee/ci/pipeline_artifact_spec.rb"
-- "./ee/spec/models/ee/ci/runner_spec.rb"
-- "./ee/spec/models/ee/merge_request_diff_spec.rb"
-- "./ee/spec/models/ee/pages_deployment_spec.rb"
-- "./ee/spec/models/ee/terraform/state_version_spec.rb"
-- "./ee/spec/models/geo/container_repository_registry_spec.rb"
-- "./ee/spec/models/geo/deleted_project_spec.rb"
-- "./ee/spec/models/geo/design_registry_spec.rb"
-- "./ee/spec/models/geo/job_artifact_registry_spec.rb"
-- "./ee/spec/models/geo_node_namespace_link_spec.rb"
-- "./ee/spec/models/geo_node_spec.rb"
-- "./ee/spec/models/geo_node_status_spec.rb"
-- "./ee/spec/models/geo/package_file_registry_spec.rb"
-- "./ee/spec/models/geo/project_registry_spec.rb"
- "./ee/spec/models/group_member_spec.rb"
-- "./ee/spec/models/group_wiki_repository_spec.rb"
-- "./ee/spec/models/merge_request_spec.rb"
-- "./ee/spec/models/packages/package_file_spec.rb"
-- "./ee/spec/models/project_spec.rb"
-- "./ee/spec/models/requirements_management/requirement_spec.rb"
-- "./ee/spec/models/snippet_repository_spec.rb"
-- "./ee/spec/models/upload_spec.rb"
-- "./ee/spec/models/visible_approvable_spec.rb"
-- "./ee/spec/policies/ci/build_policy_spec.rb"
-- "./ee/spec/presenters/approval_rule_presenter_spec.rb"
-- "./ee/spec/presenters/merge_request_presenter_spec.rb"
- "./ee/spec/replicators/geo/pipeline_artifact_replicator_spec.rb"
- "./ee/spec/replicators/geo/terraform_state_version_replicator_spec.rb"
-- "./ee/spec/requests/api/ci/pipelines_spec.rb"
-- "./ee/spec/requests/api/geo_nodes_spec.rb"
-- "./ee/spec/requests/api/geo_replication_spec.rb"
-- "./ee/spec/requests/api/graphql/mutations/dast_on_demand_scans/create_spec.rb"
-- "./ee/spec/requests/api/graphql/mutations/dast/profiles/create_spec.rb"
-- "./ee/spec/requests/api/graphql/mutations/dast/profiles/run_spec.rb"
-- "./ee/spec/requests/api/graphql/mutations/dast/profiles/update_spec.rb"
-- "./ee/spec/requests/api/graphql/project/pipeline/dast_profile_spec.rb"
-- "./ee/spec/requests/api/merge_request_approval_rules_spec.rb"
-- "./ee/spec/requests/api/merge_requests_spec.rb"
-- "./ee/spec/requests/api/project_approval_rules_spec.rb"
-- "./ee/spec/requests/api/project_approval_settings_spec.rb"
-- "./ee/spec/requests/api/project_approvals_spec.rb"
-- "./ee/spec/requests/api/project_snapshots_spec.rb"
-- "./ee/spec/requests/api/status_checks_spec.rb"
-- "./ee/spec/requests/api/vulnerability_findings_spec.rb"
-- "./ee/spec/requests/projects/merge_requests_controller_spec.rb"
-- "./ee/spec/routing/admin_routing_spec.rb"
-- "./ee/spec/serializers/dashboard_operations_project_entity_spec.rb"
-- "./ee/spec/serializers/ee/evidences/release_entity_spec.rb"
-- "./ee/spec/serializers/ee/user_serializer_spec.rb"
-- "./ee/spec/serializers/evidences/evidence_entity_spec.rb"
-- "./ee/spec/serializers/merge_request_widget_entity_spec.rb"
-- "./ee/spec/serializers/pipeline_serializer_spec.rb"
-- "./ee/spec/services/approval_rules/create_service_spec.rb"
-- "./ee/spec/services/approval_rules/finalize_service_spec.rb"
-- "./ee/spec/services/approval_rules/merge_request_rule_destroy_service_spec.rb"
-- "./ee/spec/services/approval_rules/params_filtering_service_spec.rb"
-- "./ee/spec/services/approval_rules/project_rule_destroy_service_spec.rb"
-- "./ee/spec/services/approval_rules/update_service_spec.rb"
-- "./ee/spec/services/app_sec/dast/profiles/create_service_spec.rb"
-- "./ee/spec/services/app_sec/dast/profiles/update_service_spec.rb"
-- "./ee/spec/services/app_sec/dast/scans/create_service_spec.rb"
-- "./ee/spec/services/app_sec/dast/scans/run_service_spec.rb"
-- "./ee/spec/services/ci/compare_license_scanning_reports_service_spec.rb"
-- "./ee/spec/services/ci/compare_metrics_reports_service_spec.rb"
-- "./ee/spec/services/ci/create_pipeline_service/dast_configuration_spec.rb"
- "./ee/spec/services/ci/destroy_pipeline_service_spec.rb"
-- "./ee/spec/services/ci/minutes/track_live_consumption_service_spec.rb"
-- "./ee/spec/services/ci/minutes/update_build_minutes_service_spec.rb"
-- "./ee/spec/services/ci/register_job_service_spec.rb"
- "./ee/spec/services/ci/retry_build_service_spec.rb"
-- "./ee/spec/services/ci/run_dast_scan_service_spec.rb"
- "./ee/spec/services/ci/subscribe_bridge_service_spec.rb"
-- "./ee/spec/services/ci/sync_reports_to_approval_rules_service_spec.rb"
-- "./ee/spec/services/ci/trigger_downstream_subscription_service_spec.rb"
-- "./ee/spec/services/dast_on_demand_scans/create_service_spec.rb"
- "./ee/spec/services/deployments/auto_rollback_service_spec.rb"
- "./ee/spec/services/ee/ci/job_artifacts/destroy_all_expired_service_spec.rb"
-- "./ee/spec/services/ee/ci/job_artifacts/destroy_batch_service_spec.rb"
-- "./ee/spec/services/ee/integrations/test/project_service_spec.rb"
-- "./ee/spec/services/ee/issuable/destroy_service_spec.rb"
-- "./ee/spec/services/ee/merge_requests/refresh_service_spec.rb"
-- "./ee/spec/services/ee/merge_requests/update_service_spec.rb"
-- "./ee/spec/services/ee/notification_service_spec.rb"
-- "./ee/spec/services/ee/post_receive_service_spec.rb"
-- "./ee/spec/services/ee/releases/create_evidence_service_spec.rb"
- "./ee/spec/services/ee/users/destroy_service_spec.rb"
-- "./ee/spec/services/external_status_checks/create_service_spec.rb"
-- "./ee/spec/services/external_status_checks/destroy_service_spec.rb"
-- "./ee/spec/services/external_status_checks/update_service_spec.rb"
-- "./ee/spec/services/geo/container_repository_sync_service_spec.rb"
-- "./ee/spec/services/geo/hashed_storage_migrated_event_store_spec.rb"
-- "./ee/spec/services/geo/hashed_storage_migration_service_spec.rb"
-- "./ee/spec/services/geo/node_create_service_spec.rb"
-- "./ee/spec/services/geo/node_status_request_service_spec.rb"
-- "./ee/spec/services/geo/node_update_service_spec.rb"
-- "./ee/spec/services/geo/project_housekeeping_service_spec.rb"
-- "./ee/spec/services/geo/registry_consistency_service_spec.rb"
-- "./ee/spec/services/geo/repositories_changed_event_store_spec.rb"
-- "./ee/spec/services/geo/repository_updated_event_store_spec.rb"
-- "./ee/spec/services/geo/repository_verification_reset_spec.rb"
-- "./ee/spec/services/geo/repository_verification_secondary_service_spec.rb"
-- "./ee/spec/services/merge_requests/merge_service_spec.rb"
-- "./ee/spec/services/merge_requests/reset_approvals_service_spec.rb"
-- "./ee/spec/services/merge_requests/sync_report_approver_approval_rules_spec.rb"
- "./ee/spec/services/projects/transfer_service_spec.rb"
- "./ee/spec/services/security/security_orchestration_policies/rule_schedule_service_spec.rb"
-- "./ee/spec/services/todo_service_spec.rb"
-- "./ee/spec/services/vulnerability_feedback/create_service_spec.rb"
-- "./ee/spec/services/wiki_pages/create_service_spec.rb"
-- "./ee/spec/services/wiki_pages/destroy_service_spec.rb"
-- "./ee/spec/services/wiki_pages/update_service_spec.rb"
-- "./ee/spec/support/shared_examples/fixtures/analytics_value_streams_shared_examples.rb"
-- "./ee/spec/support/shared_examples/graphql/geo/geo_registries_resolver_shared_examples.rb"
-- "./ee/spec/support/shared_examples/graphql/mutations/dast_on_demand_scans_shared_examples.rb"
-- "./ee/spec/support/shared_examples/graphql/mutations/dast_on_demand_scan_with_user_abilities_shared_examples.rb"
-- "./ee/spec/support/shared_examples/lib/gitlab/geo/geo_log_cursor_event_shared_examples.rb"
-- "./ee/spec/support/shared_examples/lib/gitlab/geo/geo_logs_event_source_info_shared_examples.rb"
-- "./ee/spec/support/shared_examples/models/concerns/blob_replicator_strategy_shared_examples.rb"
-- "./ee/spec/support/shared_examples/models/concerns/replicable_model_shared_examples.rb"
-- "./ee/spec/support/shared_examples/models/concerns/verifiable_replicator_shared_examples.rb"
-- "./ee/spec/support/shared_examples/policies/protected_environments_shared_examples.rb"
-- "./ee/spec/support/shared_examples/requests/api/project_approval_rules_api_shared_examples.rb"
-- "./ee/spec/support/shared_examples/services/audit_event_logging_shared_examples.rb"
-- "./ee/spec/support/shared_examples/services/build_execute_shared_examples.rb"
-- "./ee/spec/support/shared_examples/services/dast_on_demand_scans_shared_examples.rb"
-- "./ee/spec/support/shared_examples/services/geo_event_store_shared_examples.rb"
-- "./ee/spec/tasks/geo_rake_spec.rb"
-- "./ee/spec/tasks/gitlab/geo_rake_spec.rb"
-- "./ee/spec/workers/geo/file_download_dispatch_worker_spec.rb"
-- "./ee/spec/workers/geo/metrics_update_worker_spec.rb"
-- "./ee/spec/workers/geo/prune_event_log_worker_spec.rb"
-- "./ee/spec/workers/geo/registry_sync_worker_spec.rb"
-- "./ee/spec/workers/geo/repository_cleanup_worker_spec.rb"
-- "./ee/spec/workers/geo/repository_sync_worker_spec.rb"
-- "./ee/spec/workers/geo/repository_verification/secondary/scheduler_worker_spec.rb"
-- "./ee/spec/workers/geo/repository_verification/secondary/single_worker_spec.rb"
-- "./ee/spec/workers/geo/verification_worker_spec.rb"
-- "./ee/spec/workers/refresh_license_compliance_checks_worker_spec.rb"
- "./spec/controllers/abuse_reports_controller_spec.rb"
- "./spec/controllers/admin/spam_logs_controller_spec.rb"
- "./spec/controllers/admin/users_controller_spec.rb"
- "./spec/controllers/omniauth_callbacks_controller_spec.rb"
- "./spec/controllers/projects/issues_controller_spec.rb"
-- "./spec/controllers/projects/jobs_controller_spec.rb"
-- "./spec/controllers/projects/merge_requests/content_controller_spec.rb"
-- "./spec/controllers/projects/merge_requests_controller_spec.rb"
- "./spec/controllers/projects/pipelines_controller_spec.rb"
-- "./spec/controllers/projects/pipelines/tests_controller_spec.rb"
- "./spec/controllers/projects/settings/access_tokens_controller_spec.rb"
-- "./spec/controllers/projects/tags_controller_spec.rb"
-- "./spec/controllers/sent_notifications_controller_spec.rb"
-- "./spec/factories_spec.rb"
-- "./spec/features/action_cable_logging_spec.rb"
-- "./spec/features/admin/admin_abuse_reports_spec.rb"
-- "./spec/features/admin/admin_appearance_spec.rb"
-- "./spec/features/admin/admin_broadcast_messages_spec.rb"
-- "./spec/features/admin/admin_builds_spec.rb"
-- "./spec/features/admin/admin_dev_ops_report_spec.rb"
-- "./spec/features/admin/admin_disables_git_access_protocol_spec.rb"
-- "./spec/features/admin/admin_disables_two_factor_spec.rb"
-- "./spec/features/admin/admin_groups_spec.rb"
-- "./spec/features/admin/admin_hooks_spec.rb"
-- "./spec/features/admin/admin_labels_spec.rb"
-- "./spec/features/admin/admin_mode/login_spec.rb"
-- "./spec/features/admin/admin_mode/logout_spec.rb"
-- "./spec/features/admin/admin_mode_spec.rb"
-- "./spec/features/admin/admin_mode/workers_spec.rb"
-- "./spec/features/admin/admin_projects_spec.rb"
-- "./spec/features/admin/admin_runners_spec.rb"
-- "./spec/features/admin/admin_search_settings_spec.rb"
-- "./spec/features/admin/admin_serverless_domains_spec.rb"
-- "./spec/features/admin/admin_settings_spec.rb"
-- "./spec/features/admin/admin_users_impersonation_tokens_spec.rb"
-- "./spec/features/admin/admin_uses_repository_checks_spec.rb"
-- "./spec/features/admin/clusters/eks_spec.rb"
-- "./spec/features/admin/dashboard_spec.rb"
-- "./spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb"
-- "./spec/features/admin/users/user_spec.rb"
-- "./spec/features/admin/users/users_spec.rb"
-- "./spec/features/alert_management/alert_details_spec.rb"
-- "./spec/features/alert_management/alert_management_list_spec.rb"
-- "./spec/features/alert_management_spec.rb"
-- "./spec/features/alert_management/user_filters_alerts_by_status_spec.rb"
-- "./spec/features/alert_management/user_searches_alerts_spec.rb"
-- "./spec/features/alert_management/user_updates_alert_status_spec.rb"
-- "./spec/features/alerts_settings/user_views_alerts_settings_spec.rb"
-- "./spec/features/atom/dashboard_spec.rb"
-- "./spec/features/boards/boards_spec.rb"
-- "./spec/features/boards/focus_mode_spec.rb"
-- "./spec/features/boards/issue_ordering_spec.rb"
-- "./spec/features/boards/keyboard_shortcut_spec.rb"
-- "./spec/features/boards/multiple_boards_spec.rb"
-- "./spec/features/boards/new_issue_spec.rb"
-- "./spec/features/boards/reload_boards_on_browser_back_spec.rb"
-- "./spec/features/boards/sidebar_due_date_spec.rb"
-- "./spec/features/boards/sidebar_labels_in_namespaces_spec.rb"
-- "./spec/features/boards/sidebar_labels_spec.rb"
-- "./spec/features/boards/sidebar_milestones_spec.rb"
-- "./spec/features/boards/sidebar_spec.rb"
-- "./spec/features/boards/user_adds_lists_to_board_spec.rb"
-- "./spec/features/boards/user_visits_board_spec.rb"
-- "./spec/features/broadcast_messages_spec.rb"
-- "./spec/features/calendar_spec.rb"
-- "./spec/features/callouts/registration_enabled_spec.rb"
-- "./spec/features/clusters/cluster_detail_page_spec.rb"
-- "./spec/features/clusters/cluster_health_dashboard_spec.rb"
-- "./spec/features/commit_spec.rb"
-- "./spec/features/commits_spec.rb"
-- "./spec/features/commits/user_uses_quick_actions_spec.rb"
-- "./spec/features/contextual_sidebar_spec.rb"
-- "./spec/features/cycle_analytics_spec.rb"
-- "./spec/features/dashboard/activity_spec.rb"
-- "./spec/features/dashboard/archived_projects_spec.rb"
-- "./spec/features/dashboard/datetime_on_tooltips_spec.rb"
-- "./spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb"
-- "./spec/features/dashboard/groups_list_spec.rb"
-- "./spec/features/dashboard/group_spec.rb"
-- "./spec/features/dashboard/issues_filter_spec.rb"
-- "./spec/features/dashboard/issues_spec.rb"
-- "./spec/features/dashboard/label_filter_spec.rb"
-- "./spec/features/dashboard/merge_requests_spec.rb"
-- "./spec/features/dashboard/milestones_spec.rb"
-- "./spec/features/dashboard/project_member_activity_index_spec.rb"
-- "./spec/features/dashboard/projects_spec.rb"
-- "./spec/features/dashboard/root_spec.rb"
-- "./spec/features/dashboard/shortcuts_spec.rb"
-- "./spec/features/dashboard/snippets_spec.rb"
-- "./spec/features/dashboard/todos/todos_filtering_spec.rb"
-- "./spec/features/dashboard/todos/todos_spec.rb"
-- "./spec/features/dashboard/user_filters_projects_spec.rb"
-- "./spec/features/discussion_comments/commit_spec.rb"
-- "./spec/features/discussion_comments/issue_spec.rb"
-- "./spec/features/discussion_comments/merge_request_spec.rb"
-- "./spec/features/discussion_comments/snippets_spec.rb"
-- "./spec/features/error_pages_spec.rb"
-- "./spec/features/error_tracking/user_filters_errors_by_status_spec.rb"
-- "./spec/features/error_tracking/user_searches_sentry_errors_spec.rb"
-- "./spec/features/error_tracking/user_sees_error_details_spec.rb"
-- "./spec/features/error_tracking/user_sees_error_index_spec.rb"
-- "./spec/features/expand_collapse_diffs_spec.rb"
-- "./spec/features/explore/groups_list_spec.rb"
-- "./spec/features/explore/groups_spec.rb"
-- "./spec/features/explore/user_explores_projects_spec.rb"
-- "./spec/features/file_uploads/attachment_spec.rb"
-- "./spec/features/file_uploads/ci_artifact_spec.rb"
-- "./spec/features/file_uploads/git_lfs_spec.rb"
-- "./spec/features/file_uploads/graphql_add_design_spec.rb"
-- "./spec/features/file_uploads/group_import_spec.rb"
-- "./spec/features/file_uploads/maven_package_spec.rb"
-- "./spec/features/file_uploads/multipart_invalid_uploads_spec.rb"
-- "./spec/features/file_uploads/nuget_package_spec.rb"
-- "./spec/features/file_uploads/project_import_spec.rb"
-- "./spec/features/file_uploads/rubygem_package_spec.rb"
-- "./spec/features/file_uploads/user_avatar_spec.rb"
-- "./spec/features/frequently_visited_projects_and_groups_spec.rb"
-- "./spec/features/gitlab_experiments_spec.rb"
-- "./spec/features/global_search_spec.rb"
-- "./spec/features/groups/activity_spec.rb"
-- "./spec/features/groups/board_sidebar_spec.rb"
-- "./spec/features/groups/board_spec.rb"
-- "./spec/features/groups/clusters/eks_spec.rb"
-- "./spec/features/groups/clusters/user_spec.rb"
-- "./spec/features/groups/container_registry_spec.rb"
-- "./spec/features/groups/dependency_proxy_spec.rb"
-- "./spec/features/groups/empty_states_spec.rb"
-- "./spec/features/groups/import_export/connect_instance_spec.rb"
-- "./spec/features/groups/import_export/export_file_spec.rb"
-- "./spec/features/groups/import_export/import_file_spec.rb"
-- "./spec/features/groups/integrations/user_activates_mattermost_slash_command_spec.rb"
-- "./spec/features/groups/issues_spec.rb"
-- "./spec/features/groups/labels/index_spec.rb"
-- "./spec/features/groups/labels/search_labels_spec.rb"
-- "./spec/features/groups/labels/sort_labels_spec.rb"
-- "./spec/features/groups/labels/subscription_spec.rb"
-- "./spec/features/groups/members/filter_members_spec.rb"
-- "./spec/features/groups/members/leave_group_spec.rb"
-- "./spec/features/groups/members/list_members_spec.rb"
-- "./spec/features/groups/members/manage_groups_spec.rb"
-- "./spec/features/groups/members/manage_members_spec.rb"
-- "./spec/features/groups/members/master_adds_member_with_expiration_date_spec.rb"
-- "./spec/features/groups/members/master_manages_access_requests_spec.rb"
-- "./spec/features/groups/members/search_members_spec.rb"
-- "./spec/features/groups/members/sort_members_spec.rb"
-- "./spec/features/groups/members/tabs_spec.rb"
-- "./spec/features/groups/merge_requests_spec.rb"
-- "./spec/features/groups/milestones/gfm_autocomplete_spec.rb"
-- "./spec/features/groups/milestone_spec.rb"
-- "./spec/features/groups/milestones_sorting_spec.rb"
-- "./spec/features/groups/packages_spec.rb"
-- "./spec/features/groups/settings/group_badges_spec.rb"
-- "./spec/features/groups/settings/packages_and_registries_spec.rb"
-- "./spec/features/groups/settings/repository_spec.rb"
-- "./spec/features/groups/settings/user_searches_in_settings_spec.rb"
-- "./spec/features/groups/show_spec.rb"
-- "./spec/features/groups_spec.rb"
-- "./spec/features/groups/user_browse_projects_group_page_spec.rb"
-- "./spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb"
-- "./spec/features/help_pages_spec.rb"
-- "./spec/features/ide_spec.rb"
-- "./spec/features/ide/user_commits_changes_spec.rb"
-- "./spec/features/ide/user_opens_merge_request_spec.rb"
-- "./spec/features/import/manifest_import_spec.rb"
-- "./spec/features/incidents/incident_details_spec.rb"
-- "./spec/features/incidents/incidents_list_spec.rb"
-- "./spec/features/incidents/user_creates_new_incident_spec.rb"
-- "./spec/features/incidents/user_filters_incidents_by_status_spec.rb"
-- "./spec/features/incidents/user_searches_incidents_spec.rb"
-- "./spec/features/incidents/user_views_incident_spec.rb"
-- "./spec/features/issuables/issuable_list_spec.rb"
-- "./spec/features/issuables/markdown_references/internal_references_spec.rb"
-- "./spec/features/issuables/markdown_references/jira_spec.rb"
-- "./spec/features/issuables/sorting_list_spec.rb"
-- "./spec/features/issuables/user_sees_sidebar_spec.rb"
-- "./spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb"
-- "./spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb"
-- "./spec/features/issues/csv_spec.rb"
-- "./spec/features/issues/discussion_lock_spec.rb"
-- "./spec/features/issues/filtered_search/dropdown_assignee_spec.rb"
-- "./spec/features/issues/filtered_search/dropdown_author_spec.rb"
-- "./spec/features/issues/filtered_search/dropdown_base_spec.rb"
-- "./spec/features/issues/filtered_search/dropdown_emoji_spec.rb"
-- "./spec/features/issues/filtered_search/dropdown_hint_spec.rb"
-- "./spec/features/issues/filtered_search/dropdown_label_spec.rb"
-- "./spec/features/issues/filtered_search/dropdown_milestone_spec.rb"
-- "./spec/features/issues/filtered_search/dropdown_release_spec.rb"
-- "./spec/features/issues/filtered_search/filter_issues_spec.rb"
-- "./spec/features/issues/filtered_search/recent_searches_spec.rb"
-- "./spec/features/issues/filtered_search/search_bar_spec.rb"
-- "./spec/features/issues/filtered_search/visual_tokens_spec.rb"
-- "./spec/features/issues/form_spec.rb"
-- "./spec/features/issues/gfm_autocomplete_spec.rb"
-- "./spec/features/issues/group_label_sidebar_spec.rb"
-- "./spec/features/issues/incident_issue_spec.rb"
- "./spec/features/issues/issue_detail_spec.rb"
-- "./spec/features/issues/issue_header_spec.rb"
-- "./spec/features/issues/issue_sidebar_spec.rb"
-- "./spec/features/issues/keyboard_shortcut_spec.rb"
-- "./spec/features/issues/markdown_toolbar_spec.rb"
-- "./spec/features/issues/move_spec.rb"
-- "./spec/features/issues/note_polling_spec.rb"
-- "./spec/features/issues/notes_on_issues_spec.rb"
-- "./spec/features/issues/related_issues_spec.rb"
-- "./spec/features/issues/resource_label_events_spec.rb"
-- "./spec/features/issues/service_desk_spec.rb"
-- "./spec/features/issues/spam_issues_spec.rb"
-- "./spec/features/issues/todo_spec.rb"
-- "./spec/features/issues/user_bulk_edits_issues_labels_spec.rb"
-- "./spec/features/issues/user_bulk_edits_issues_spec.rb"
-- "./spec/features/issues/user_comments_on_issue_spec.rb"
-- "./spec/features/issues/user_creates_branch_and_merge_request_spec.rb"
-- "./spec/features/issues/user_creates_confidential_merge_request_spec.rb"
-- "./spec/features/issues/user_creates_issue_by_email_spec.rb"
-- "./spec/features/issues/user_creates_issue_spec.rb"
-- "./spec/features/issues/user_edits_issue_spec.rb"
-- "./spec/features/issues/user_filters_issues_spec.rb"
-- "./spec/features/issues/user_interacts_with_awards_spec.rb"
-- "./spec/features/issues/user_invites_from_a_comment_spec.rb"
-- "./spec/features/issues/user_resets_their_incoming_email_token_spec.rb"
-- "./spec/features/issues/user_sees_empty_state_spec.rb"
-- "./spec/features/issues/user_sees_live_update_spec.rb"
-- "./spec/features/issues/user_sees_sidebar_updates_in_realtime_spec.rb"
-- "./spec/features/issues/user_sorts_issue_comments_spec.rb"
-- "./spec/features/issues/user_sorts_issues_spec.rb"
-- "./spec/features/issues/user_toggles_subscription_spec.rb"
-- "./spec/features/issues/user_uses_quick_actions_spec.rb"
-- "./spec/features/issues/user_views_issue_spec.rb"
-- "./spec/features/issues/user_views_issues_spec.rb"
-- "./spec/features/jira_connect/branches_spec.rb"
-- "./spec/features/labels_hierarchy_spec.rb"
-- "./spec/features/markdown/copy_as_gfm_spec.rb"
-- "./spec/features/markdown/gitlab_flavored_markdown_spec.rb"
-- "./spec/features/markdown/keyboard_shortcuts_spec.rb"
-- "./spec/features/markdown/math_spec.rb"
-- "./spec/features/markdown/mermaid_spec.rb"
-- "./spec/features/markdown/metrics_spec.rb"
-- "./spec/features/merge_request/batch_comments_spec.rb"
-- "./spec/features/merge_request/close_reopen_report_toggle_spec.rb"
-- "./spec/features/merge_request/maintainer_edits_fork_spec.rb"
-- "./spec/features/merge_request/merge_request_discussion_lock_spec.rb"
-- "./spec/features/merge_requests/filters_generic_behavior_spec.rb"
-- "./spec/features/merge_requests/user_exports_as_csv_spec.rb"
-- "./spec/features/merge_requests/user_filters_by_approvals_spec.rb"
-- "./spec/features/merge_requests/user_filters_by_assignees_spec.rb"
-- "./spec/features/merge_requests/user_filters_by_deployments_spec.rb"
-- "./spec/features/merge_requests/user_filters_by_draft_spec.rb"
-- "./spec/features/merge_requests/user_filters_by_labels_spec.rb"
-- "./spec/features/merge_requests/user_filters_by_milestones_spec.rb"
-- "./spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb"
-- "./spec/features/merge_requests/user_filters_by_target_branch_spec.rb"
-- "./spec/features/merge_requests/user_mass_updates_spec.rb"
-- "./spec/features/merge_request/user_accepts_merge_request_spec.rb"
-- "./spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb"
-- "./spec/features/merge_request/user_approves_spec.rb"
-- "./spec/features/merge_request/user_assigns_themselves_spec.rb"
-- "./spec/features/merge_request/user_awards_emoji_spec.rb"
-- "./spec/features/merge_request/user_clicks_merge_request_tabs_spec.rb"
-- "./spec/features/merge_request/user_comments_on_commit_spec.rb"
-- "./spec/features/merge_request/user_comments_on_diff_spec.rb"
-- "./spec/features/merge_request/user_comments_on_merge_request_spec.rb"
-- "./spec/features/merge_request/user_creates_image_diff_notes_spec.rb"
-- "./spec/features/merge_request/user_creates_merge_request_spec.rb"
-- "./spec/features/merge_request/user_creates_mr_spec.rb"
-- "./spec/features/merge_request/user_customizes_merge_commit_message_spec.rb"
-- "./spec/features/merge_request/user_edits_assignees_sidebar_spec.rb"
-- "./spec/features/merge_request/user_edits_merge_request_spec.rb"
-- "./spec/features/merge_request/user_edits_mr_spec.rb"
-- "./spec/features/merge_request/user_edits_reviewers_sidebar_spec.rb"
-- "./spec/features/merge_request/user_expands_diff_spec.rb"
-- "./spec/features/merge_request/user_interacts_with_batched_mr_diffs_spec.rb"
-- "./spec/features/merge_request/user_invites_from_a_comment_spec.rb"
-- "./spec/features/merge_request/user_jumps_to_discussion_spec.rb"
-- "./spec/features/merge_request/user_locks_discussion_spec.rb"
-- "./spec/features/merge_request/user_manages_subscription_spec.rb"
-- "./spec/features/merge_request/user_marks_merge_request_as_draft_spec.rb"
-- "./spec/features/merge_request/user_merges_immediately_spec.rb"
-- "./spec/features/merge_request/user_merges_merge_request_spec.rb"
-- "./spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb"
-- "./spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb"
-- "./spec/features/merge_request/user_posts_diff_notes_spec.rb"
-- "./spec/features/merge_request/user_posts_notes_spec.rb"
-- "./spec/features/merge_request/user_rebases_merge_request_spec.rb"
-- "./spec/features/merge_request/user_resolves_conflicts_spec.rb"
-- "./spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb"
-- "./spec/features/merge_request/user_resolves_outdated_diff_discussions_spec.rb"
-- "./spec/features/merge_request/user_resolves_wip_mr_spec.rb"
-- "./spec/features/merge_request/user_reverts_merge_request_spec.rb"
-- "./spec/features/merge_request/user_reviews_image_spec.rb"
-- "./spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb"
-- "./spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb"
-- "./spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb"
-- "./spec/features/merge_request/user_sees_cherry_pick_modal_spec.rb"
-- "./spec/features/merge_request/user_sees_closing_issues_message_spec.rb"
-- "./spec/features/merge_request/user_sees_deleted_target_branch_spec.rb"
-- "./spec/features/merge_request/user_sees_deployment_widget_spec.rb"
-- "./spec/features/merge_request/user_sees_diff_spec.rb"
-- "./spec/features/merge_request/user_sees_discussions_spec.rb"
-- "./spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb"
-- "./spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb"
-- "./spec/features/merge_request/user_sees_merge_widget_spec.rb"
-- "./spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb"
-- "./spec/features/merge_request/user_sees_mr_from_deleted_forked_project_spec.rb"
-- "./spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb"
-- "./spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb"
-- "./spec/features/merge_request/user_sees_pipelines_from_forked_project_spec.rb"
-- "./spec/features/merge_request/user_sees_pipelines_spec.rb"
-- "./spec/features/merge_request/user_sees_suggest_pipeline_spec.rb"
-- "./spec/features/merge_request/user_sees_system_notes_spec.rb"
-- "./spec/features/merge_request/user_sees_versions_spec.rb"
-- "./spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb"
-- "./spec/features/merge_request/user_squashes_merge_request_spec.rb"
-- "./spec/features/merge_request/user_suggests_changes_on_diff_spec.rb"
-- "./spec/features/merge_request/user_toggles_whitespace_changes_spec.rb"
-- "./spec/features/merge_request/user_uses_quick_actions_spec.rb"
-- "./spec/features/merge_request/user_views_auto_expanding_diff_spec.rb"
-- "./spec/features/merge_request/user_views_diffs_commit_spec.rb"
-- "./spec/features/merge_request/user_views_diffs_file_by_file_spec.rb"
-- "./spec/features/merge_request/user_views_diffs_spec.rb"
-- "./spec/features/merge_request/user_views_open_merge_request_spec.rb"
-- "./spec/features/merge_request/user_views_user_status_on_merge_request_spec.rb"
-- "./spec/features/milestone_spec.rb"
-- "./spec/features/milestones/user_creates_milestone_spec.rb"
-- "./spec/features/milestones/user_deletes_milestone_spec.rb"
-- "./spec/features/milestones/user_edits_milestone_spec.rb"
-- "./spec/features/milestones/user_views_milestone_spec.rb"
-- "./spec/features/milestones/user_views_milestones_spec.rb"
-- "./spec/features/nav/top_nav_responsive_spec.rb"
-- "./spec/features/oauth_login_spec.rb"
-- "./spec/features/participants_autocomplete_spec.rb"
-- "./spec/features/populate_new_pipeline_vars_with_params_spec.rb"
-- "./spec/features/profiles/account_spec.rb"
-- "./spec/features/profiles/active_sessions_spec.rb"
-- "./spec/features/profiles/keys_spec.rb"
-- "./spec/features/profiles/oauth_applications_spec.rb"
-- "./spec/features/profile_spec.rb"
-- "./spec/features/profiles/personal_access_tokens_spec.rb"
-- "./spec/features/profiles/user_changes_notified_of_own_activity_spec.rb"
-- "./spec/features/profiles/user_edit_preferences_spec.rb"
-- "./spec/features/profiles/user_edit_profile_spec.rb"
-- "./spec/features/profiles/user_search_settings_spec.rb"
-- "./spec/features/profiles/user_visits_notifications_tab_spec.rb"
-- "./spec/features/profiles/user_visits_profile_preferences_page_spec.rb"
-- "./spec/features/profiles/user_visits_profile_spec.rb"
-- "./spec/features/project_group_variables_spec.rb"
-- "./spec/features/projects/activity/user_sees_activity_spec.rb"
-- "./spec/features/projects/activity/user_sees_design_activity_spec.rb"
-- "./spec/features/projects/activity/user_sees_design_comment_spec.rb"
-- "./spec/features/projects/activity/user_sees_private_activity_spec.rb"
-- "./spec/features/projects/artifacts/file_spec.rb"
-- "./spec/features/projects/artifacts/raw_spec.rb"
-- "./spec/features/projects/artifacts/user_browses_artifacts_spec.rb"
-- "./spec/features/projects/badges/list_spec.rb"
-- "./spec/features/projects/badges/pipeline_badge_spec.rb"
-- "./spec/features/projects/blobs/balsamiq_spec.rb"
-- "./spec/features/projects/blobs/blob_line_permalink_updater_spec.rb"
-- "./spec/features/projects/blobs/blob_show_spec.rb"
-- "./spec/features/projects/blobs/edit_spec.rb"
-- "./spec/features/projects/blobs/shortcuts_blob_spec.rb"
-- "./spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb"
-- "./spec/features/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb"
-- "./spec/features/projects/blobs/user_views_pipeline_editor_button_spec.rb"
-- "./spec/features/projects/branches/new_branch_ref_dropdown_spec.rb"
-- "./spec/features/projects/branches_spec.rb"
-- "./spec/features/projects/branches/user_creates_branch_spec.rb"
-- "./spec/features/projects/branches/user_deletes_branch_spec.rb"
-- "./spec/features/projects/branches/user_views_branches_spec.rb"
-- "./spec/features/projects/ci/editor_spec.rb"
-- "./spec/features/projects/clusters/eks_spec.rb"
-- "./spec/features/projects/clusters/gcp_spec.rb"
-- "./spec/features/projects/clusters_spec.rb"
-- "./spec/features/projects/clusters/user_spec.rb"
-- "./spec/features/projects/commit/builds_spec.rb"
-- "./spec/features/projects/commit/cherry_pick_spec.rb"
-- "./spec/features/projects/commit/comments/user_adds_comment_spec.rb"
-- "./spec/features/projects/commit/comments/user_deletes_comments_spec.rb"
-- "./spec/features/projects/commit/comments/user_edits_comments_spec.rb"
-- "./spec/features/projects/commit/diff_notes_spec.rb"
-- "./spec/features/projects/commit/mini_pipeline_graph_spec.rb"
-- "./spec/features/projects/commits/user_browses_commits_spec.rb"
-- "./spec/features/projects/commit/user_comments_on_commit_spec.rb"
-- "./spec/features/projects/commit/user_reverts_commit_spec.rb"
-- "./spec/features/projects/commit/user_views_user_status_on_commit_spec.rb"
-- "./spec/features/projects/compare_spec.rb"
-- "./spec/features/projects/container_registry_spec.rb"
-- "./spec/features/projects/deploy_keys_spec.rb"
-- "./spec/features/projects/diffs/diff_show_spec.rb"
-- "./spec/features/projects/environments/environment_metrics_spec.rb"
-- "./spec/features/projects/environments/environment_spec.rb"
-- "./spec/features/projects/environments/environments_spec.rb"
-- "./spec/features/projects/environments_pod_logs_spec.rb"
-- "./spec/features/projects/feature_flags/user_creates_feature_flag_spec.rb"
-- "./spec/features/projects/feature_flags/user_deletes_feature_flag_spec.rb"
-- "./spec/features/projects/feature_flags/user_sees_feature_flag_list_spec.rb"
-- "./spec/features/projects/feature_flags/user_updates_feature_flag_spec.rb"
-- "./spec/features/projects/feature_flag_user_lists/user_deletes_feature_flag_user_list_spec.rb"
-- "./spec/features/projects/feature_flag_user_lists/user_edits_feature_flag_user_list_spec.rb"
-- "./spec/features/projects/feature_flag_user_lists/user_sees_feature_flag_user_list_details_spec.rb"
-- "./spec/features/projects/features_visibility_spec.rb"
-- "./spec/features/projects/files/dockerfile_dropdown_spec.rb"
-- "./spec/features/projects/files/edit_file_soft_wrap_spec.rb"
-- "./spec/features/projects/files/files_sort_submodules_with_folders_spec.rb"
-- "./spec/features/projects/files/find_file_keyboard_spec.rb"
-- "./spec/features/projects/files/gitignore_dropdown_spec.rb"
-- "./spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb"
-- "./spec/features/projects/files/project_owner_creates_license_file_spec.rb"
-- "./spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb"
-- "./spec/features/projects/files/template_selector_menu_spec.rb"
-- "./spec/features/projects/files/template_type_dropdown_spec.rb"
-- "./spec/features/projects/files/undo_template_spec.rb"
-- "./spec/features/projects/files/user_browses_a_tree_with_a_folder_containing_only_a_folder_spec.rb"
-- "./spec/features/projects/files/user_browses_files_spec.rb"
-- "./spec/features/projects/files/user_browses_lfs_files_spec.rb"
-- "./spec/features/projects/files/user_creates_directory_spec.rb"
-- "./spec/features/projects/files/user_creates_files_spec.rb"
-- "./spec/features/projects/files/user_deletes_files_spec.rb"
-- "./spec/features/projects/files/user_edits_files_spec.rb"
-- "./spec/features/projects/files/user_find_file_spec.rb"
-- "./spec/features/projects/files/user_reads_pipeline_status_spec.rb"
-- "./spec/features/projects/files/user_replaces_files_spec.rb"
-- "./spec/features/projects/files/user_uploads_files_spec.rb"
-- "./spec/features/projects/fork_spec.rb"
-- "./spec/features/projects/gfm_autocomplete_load_spec.rb"
-- "./spec/features/projects/graph_spec.rb"
-- "./spec/features/projects/import_export/export_file_spec.rb"
-- "./spec/features/projects/import_export/import_file_spec.rb"
-- "./spec/features/projects/infrastructure_registry_spec.rb"
-- "./spec/features/projects/integrations/user_activates_asana_spec.rb"
-- "./spec/features/projects/integrations/user_activates_assembla_spec.rb"
-- "./spec/features/projects/integrations/user_activates_atlassian_bamboo_ci_spec.rb"
-- "./spec/features/projects/integrations/user_activates_flowdock_spec.rb"
-- "./spec/features/projects/integrations/user_activates_jira_spec.rb"
-- "./spec/features/projects/integrations/user_activates_pivotaltracker_spec.rb"
-- "./spec/features/projects/integrations/user_uses_inherited_settings_spec.rb"
-- "./spec/features/projects/issuable_templates_spec.rb"
-- "./spec/features/projects/issues/design_management/user_paginates_designs_spec.rb"
-- "./spec/features/projects/issues/design_management/user_permissions_upload_spec.rb"
-- "./spec/features/projects/issues/design_management/user_uploads_designs_spec.rb"
-- "./spec/features/projects/issues/design_management/user_views_design_spec.rb"
-- "./spec/features/projects/issues/design_management/user_views_designs_spec.rb"
-- "./spec/features/projects/issues/design_management/user_views_designs_with_svg_xss_spec.rb"
-- "./spec/features/projects/issues/email_participants_spec.rb"
-- "./spec/features/projects/jobs/permissions_spec.rb"
-- "./spec/features/projects/jobs_spec.rb"
-- "./spec/features/projects/jobs/user_browses_job_spec.rb"
-- "./spec/features/projects/jobs/user_browses_jobs_spec.rb"
-- "./spec/features/projects/labels/issues_sorted_by_priority_spec.rb"
-- "./spec/features/projects/labels/search_labels_spec.rb"
-- "./spec/features/projects/labels/sort_labels_spec.rb"
-- "./spec/features/projects/labels/subscription_spec.rb"
-- "./spec/features/projects/labels/update_prioritization_spec.rb"
-- "./spec/features/projects/labels/user_removes_labels_spec.rb"
-- "./spec/features/projects/members/anonymous_user_sees_members_spec.rb"
-- "./spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb"
-- "./spec/features/projects/members/group_members_spec.rb"
-- "./spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb"
-- "./spec/features/projects/members/groups_with_access_list_spec.rb"
-- "./spec/features/projects/members/invite_group_spec.rb"
-- "./spec/features/projects/members/list_spec.rb"
-- "./spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb"
-- "./spec/features/projects/members/master_manages_access_requests_spec.rb"
-- "./spec/features/projects/members/sorting_spec.rb"
-- "./spec/features/projects/members/tabs_spec.rb"
-- "./spec/features/projects/members/user_requests_access_spec.rb"
-- "./spec/features/projects/merge_request_button_spec.rb"
-- "./spec/features/projects/milestones/gfm_autocomplete_spec.rb"
-- "./spec/features/projects/milestones/milestones_sorting_spec.rb"
-- "./spec/features/projects/milestones/new_spec.rb"
-- "./spec/features/projects/milestones/user_interacts_with_labels_spec.rb"
-- "./spec/features/projects/network_graph_spec.rb"
-- "./spec/features/projects/new_project_from_template_spec.rb"
-- "./spec/features/projects/new_project_spec.rb"
-- "./spec/features/projects/packages_spec.rb"
-- "./spec/features/projects/pages/user_adds_domain_spec.rb"
-- "./spec/features/projects/pages/user_edits_lets_encrypt_settings_spec.rb"
-- "./spec/features/projects/pages/user_edits_settings_spec.rb"
-- "./spec/features/projects/pipeline_schedules_spec.rb"
- "./spec/features/projects/pipelines/pipeline_spec.rb"
-- "./spec/features/projects/pipelines/pipelines_spec.rb"
-- "./spec/features/projects/product_analytics/graphs_spec.rb"
-- "./spec/features/projects/releases/user_creates_release_spec.rb"
-- "./spec/features/projects/releases/user_views_edit_release_spec.rb"
-- "./spec/features/projects/releases/user_views_release_spec.rb"
-- "./spec/features/projects/releases/user_views_releases_spec.rb"
-- "./spec/features/projects/remote_mirror_spec.rb"
-- "./spec/features/projects/serverless/functions_spec.rb"
-- "./spec/features/projects/services/disable_triggers_spec.rb"
-- "./spec/features/projects/services/prometheus_external_alerts_spec.rb"
-- "./spec/features/projects/services/user_activates_emails_on_push_spec.rb"
-- "./spec/features/projects/services/user_activates_irker_spec.rb"
-- "./spec/features/projects/services/user_activates_issue_tracker_spec.rb"
-- "./spec/features/projects/services/user_activates_jetbrains_teamcity_ci_spec.rb"
-- "./spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb"
-- "./spec/features/projects/services/user_activates_packagist_spec.rb"
-- "./spec/features/projects/services/user_activates_prometheus_spec.rb"
-- "./spec/features/projects/services/user_activates_pushover_spec.rb"
-- "./spec/features/projects/services/user_activates_slack_notifications_spec.rb"
-- "./spec/features/projects/services/user_activates_slack_slash_command_spec.rb"
-- "./spec/features/projects/services/user_views_services_spec.rb"
-- "./spec/features/projects/settings/access_tokens_spec.rb"
-- "./spec/features/projects/settings/lfs_settings_spec.rb"
-- "./spec/features/projects/settings/monitor_settings_spec.rb"
-- "./spec/features/projects/settings/packages_settings_spec.rb"
-- "./spec/features/projects/settings/project_badges_spec.rb"
-- "./spec/features/projects/settings/project_settings_spec.rb"
-- "./spec/features/projects/settings/registry_settings_spec.rb"
-- "./spec/features/projects/settings/repository_settings_spec.rb"
-- "./spec/features/projects/settings/service_desk_setting_spec.rb"
-- "./spec/features/projects/settings/user_changes_default_branch_spec.rb"
-- "./spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb"
-- "./spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb"
-- "./spec/features/projects/settings/user_manages_project_members_spec.rb"
-- "./spec/features/projects/settings/user_searches_in_settings_spec.rb"
-- "./spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb"
-- "./spec/features/projects/settings/user_tags_project_spec.rb"
-- "./spec/features/projects/settings/user_transfers_a_project_spec.rb"
-- "./spec/features/projects/settings/visibility_settings_spec.rb"
-- "./spec/features/projects/settings/webhooks_settings_spec.rb"
-- "./spec/features/projects/show/schema_markup_spec.rb"
-- "./spec/features/projects/show/user_interacts_with_auto_devops_banner_spec.rb"
-- "./spec/features/projects/show/user_interacts_with_stars_spec.rb"
-- "./spec/features/projects/show/user_manages_notifications_spec.rb"
-- "./spec/features/projects/show/user_sees_collaboration_links_spec.rb"
-- "./spec/features/projects/show/user_sees_last_commit_ci_status_spec.rb"
-- "./spec/features/projects/show/user_sees_readme_spec.rb"
-- "./spec/features/projects/show/user_uploads_files_spec.rb"
-- "./spec/features/projects/snippets/create_snippet_spec.rb"
-- "./spec/features/projects/snippets/show_spec.rb"
-- "./spec/features/projects/snippets/user_comments_on_snippet_spec.rb"
-- "./spec/features/projects/snippets/user_deletes_snippet_spec.rb"
-- "./spec/features/projects/snippets/user_updates_snippet_spec.rb"
-- "./spec/features/projects_spec.rb"
-- "./spec/features/projects/sub_group_issuables_spec.rb"
-- "./spec/features/projects/tags/user_edits_tags_spec.rb"
-- "./spec/features/projects/terraform_spec.rb"
-- "./spec/features/projects/tree/create_directory_spec.rb"
-- "./spec/features/projects/tree/create_file_spec.rb"
-- "./spec/features/projects/tree/tree_show_spec.rb"
-- "./spec/features/projects/tree/upload_file_spec.rb"
-- "./spec/features/projects/user_changes_project_visibility_spec.rb"
-- "./spec/features/projects/user_creates_project_spec.rb"
-- "./spec/features/projects/user_sees_sidebar_spec.rb"
-- "./spec/features/projects/user_sees_user_popover_spec.rb"
-- "./spec/features/projects/user_uses_shortcuts_spec.rb"
-- "./spec/features/projects/user_views_empty_project_spec.rb"
-- "./spec/features/projects/view_on_env_spec.rb"
-- "./spec/features/projects/wikis_spec.rb"
-- "./spec/features/projects/wiki/user_views_wiki_empty_spec.rb"
-- "./spec/features/project_variables_spec.rb"
-- "./spec/features/promotion_spec.rb"
-- "./spec/features/protected_branches_spec.rb"
-- "./spec/features/protected_tags_spec.rb"
-- "./spec/features/reportable_note/commit_spec.rb"
-- "./spec/features/reportable_note/issue_spec.rb"
-- "./spec/features/reportable_note/merge_request_spec.rb"
-- "./spec/features/reportable_note/snippets_spec.rb"
-- "./spec/features/runners_spec.rb"
-- "./spec/features/search/user_searches_for_code_spec.rb"
-- "./spec/features/search/user_searches_for_commits_spec.rb"
-- "./spec/features/search/user_searches_for_issues_spec.rb"
-- "./spec/features/search/user_searches_for_merge_requests_spec.rb"
-- "./spec/features/search/user_searches_for_milestones_spec.rb"
-- "./spec/features/search/user_searches_for_projects_spec.rb"
-- "./spec/features/search/user_searches_for_users_spec.rb"
-- "./spec/features/search/user_searches_for_wiki_pages_spec.rb"
-- "./spec/features/search/user_uses_header_search_field_spec.rb"
-- "./spec/features/search/user_uses_search_filters_spec.rb"
- "./spec/features/signed_commits_spec.rb"
-- "./spec/features/snippets/embedded_snippet_spec.rb"
-- "./spec/features/snippets/internal_snippet_spec.rb"
-- "./spec/features/snippets/notes_on_personal_snippets_spec.rb"
-- "./spec/features/snippets/private_snippets_spec.rb"
-- "./spec/features/snippets/public_snippets_spec.rb"
-- "./spec/features/snippets/show_spec.rb"
-- "./spec/features/snippets/user_creates_snippet_spec.rb"
-- "./spec/features/snippets/user_deletes_snippet_spec.rb"
-- "./spec/features/snippets/user_edits_snippet_spec.rb"
-- "./spec/features/tags/developer_creates_tag_spec.rb"
-- "./spec/features/tags/developer_deletes_tag_spec.rb"
-- "./spec/features/tags/developer_updates_tag_spec.rb"
-- "./spec/features/task_lists_spec.rb"
-- "./spec/features/triggers_spec.rb"
-- "./spec/features/u2f_spec.rb"
-- "./spec/features/uploads/user_uploads_avatar_to_profile_spec.rb"
-- "./spec/features/uploads/user_uploads_file_to_note_spec.rb"
-- "./spec/features/user_can_display_performance_bar_spec.rb"
-- "./spec/features/user_opens_link_to_comment_spec.rb"
-- "./spec/features/user_sees_revert_modal_spec.rb"
-- "./spec/features/users/login_spec.rb"
-- "./spec/features/users/logout_spec.rb"
-- "./spec/features/users/overview_spec.rb"
-- "./spec/features/users/signup_spec.rb"
-- "./spec/features/users/snippets_spec.rb"
-- "./spec/features/users/terms_spec.rb"
-- "./spec/features/users/user_browses_projects_on_user_page_spec.rb"
-- "./spec/features/webauthn_spec.rb"
-- "./spec/features/whats_new_spec.rb"
-- "./spec/finders/ci/pipeline_schedules_finder_spec.rb"
-- "./spec/finders/ci/pipelines_finder_spec.rb"
-- "./spec/finders/ci/pipelines_for_merge_request_finder_spec.rb"
-- "./spec/finders/projects_finder_spec.rb"
-- "./spec/finders/releases/evidence_pipeline_finder_spec.rb"
-- "./spec/frontend/fixtures/analytics.rb"
-- "./spec/frontend/fixtures/jobs.rb"
-- "./spec/frontend/fixtures/pipeline_schedules.rb"
-- "./spec/frontend/fixtures/pipelines.rb"
-- "./spec/graphql/mutations/design_management/upload_spec.rb"
-- "./spec/graphql/mutations/merge_requests/accept_spec.rb"
-- "./spec/graphql/resolvers/ci/test_report_summary_resolver_spec.rb"
- "./spec/helpers/issuables_helper_spec.rb"
-- "./spec/initializers/active_record_locking_spec.rb"
-- "./spec/initializers/database_config_spec.rb"
- "./spec/lib/gitlab/auth_spec.rb"
-- "./spec/lib/gitlab/ci/badge/pipeline/status_spec.rb"
-- "./spec/lib/gitlab/ci/build/policy/changes_spec.rb"
-- "./spec/lib/gitlab/ci/charts_spec.rb"
-- "./spec/lib/gitlab/ci/config_spec.rb"
- "./spec/lib/gitlab/ci/pipeline/chain/create_spec.rb"
- "./spec/lib/gitlab/ci/pipeline/chain/seed_block_spec.rb"
- "./spec/lib/gitlab/ci/pipeline/seed/build_spec.rb"
-- "./spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb"
-- "./spec/lib/gitlab/ci/status/stage/common_spec.rb"
-- "./spec/lib/gitlab/ci/status/stage/factory_spec.rb"
-- "./spec/lib/gitlab/ci/status/stage/play_manual_spec.rb"
- "./spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb"
-- "./spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb"
- "./spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb"
- "./spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb"
+- "./spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb"
- "./spec/lib/gitlab/ci/templates/managed_cluster_applications_gitlab_ci_yaml_spec.rb"
-- "./spec/lib/gitlab/database/bulk_update_spec.rb"
-- "./spec/lib/gitlab/database/connection_spec.rb"
-- "./spec/lib/gitlab/database/load_balancing/host_spec.rb"
-- "./spec/lib/gitlab/database/load_balancing_spec.rb"
-- "./spec/lib/gitlab/database/postgresql_adapter/force_disconnectable_mixin_spec.rb"
-- "./spec/lib/gitlab/database/postgresql_adapter/type_map_cache_spec.rb"
-- "./spec/lib/gitlab/database/schema_migrations/context_spec.rb"
-- "./spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb"
-- "./spec/lib/gitlab/database/with_lock_retries_spec.rb"
-- "./spec/lib/gitlab/data_builder/pipeline_spec.rb"
- "./spec/lib/gitlab/email/handler/create_issue_handler_spec.rb"
- "./spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb"
- "./spec/lib/gitlab/email/handler/create_note_handler_spec.rb"
- "./spec/lib/gitlab/email/handler/create_note_on_issuable_handler_spec.rb"
-- "./spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb"
-- "./spec/lib/gitlab/usage_data_spec.rb"
- "./spec/lib/peek/views/active_record_spec.rb"
-- "./spec/mailers/emails/pipelines_spec.rb"
-- "./spec/migrations/20210205174154_remove_bad_dependency_proxy_manifests_spec.rb"
-- "./spec/migrations/20210722150102_operations_feature_flags_correct_flexible_rollout_values_spec.rb"
-- "./spec/migrations/backfill_escalation_policies_for_oncall_schedules_spec.rb"
-- "./spec/migrations/insert_ci_daily_pipeline_schedule_triggers_plan_limits_spec.rb"
-- "./spec/migrations/remove_duplicate_dast_site_tokens_with_same_token_spec.rb"
-- "./spec/models/ci/bridge_spec.rb"
- "./spec/models/ci/build_need_spec.rb"
-- "./spec/models/ci/build_spec.rb"
- "./spec/models/ci/build_trace_chunk_spec.rb"
-- "./spec/models/ci/commit_with_pipeline_spec.rb"
-- "./spec/models/ci/group_spec.rb"
- "./spec/models/ci/group_variable_spec.rb"
-- "./spec/models/ci/instance_variable_spec.rb"
- "./spec/models/ci/job_artifact_spec.rb"
- "./spec/models/ci/job_variable_spec.rb"
-- "./spec/models/ci/legacy_stage_spec.rb"
-- "./spec/models/ci/pipeline_schedule_spec.rb"
- "./spec/models/ci/pipeline_spec.rb"
-- "./spec/models/ci/runner_namespace_spec.rb"
-- "./spec/models/ci/runner_project_spec.rb"
- "./spec/models/ci/runner_spec.rb"
-- "./spec/models/ci/running_build_spec.rb"
-- "./spec/models/ci/stage_spec.rb"
- "./spec/models/ci/variable_spec.rb"
-- "./spec/models/clusters/applications/jupyter_spec.rb"
- "./spec/models/clusters/applications/runner_spec.rb"
-- "./spec/models/commit_collection_spec.rb"
- "./spec/models/commit_status_spec.rb"
- "./spec/models/concerns/batch_destroy_dependent_associations_spec.rb"
- "./spec/models/concerns/bulk_insertable_associations_spec.rb"
-- "./spec/models/concerns/cron_schedulable_spec.rb"
- "./spec/models/concerns/has_environment_scope_spec.rb"
-- "./spec/models/concerns/schedulable_spec.rb"
- "./spec/models/concerns/token_authenticatable_spec.rb"
- "./spec/models/design_management/version_spec.rb"
-- "./spec/models/environment_status_spec.rb"
- "./spec/models/hooks/system_hook_spec.rb"
-- "./spec/models/issue_spec.rb"
- "./spec/models/members/project_member_spec.rb"
-- "./spec/models/merge_request_spec.rb"
-- "./spec/models/plan_spec.rb"
-- "./spec/models/project_feature_usage_spec.rb"
-- "./spec/models/project_spec.rb"
- "./spec/models/spam_log_spec.rb"
- "./spec/models/user_spec.rb"
- "./spec/models/user_status_spec.rb"
-- "./spec/policies/ci/build_policy_spec.rb"
-- "./spec/policies/ci/pipeline_policy_spec.rb"
-- "./spec/presenters/ci/stage_presenter_spec.rb"
-- "./spec/requests/api/admin/ci/variables_spec.rb"
-- "./spec/requests/api/admin/plan_limits_spec.rb"
-- "./spec/requests/api/ci/jobs_spec.rb"
- "./spec/requests/api/ci/pipeline_schedules_spec.rb"
- "./spec/requests/api/ci/pipelines_spec.rb"
-- "./spec/requests/api/ci/runner/runners_post_spec.rb"
-- "./spec/requests/api/ci/runners_spec.rb"
-- "./spec/requests/api/commits_spec.rb"
- "./spec/requests/api/commit_statuses_spec.rb"
-- "./spec/requests/api/graphql/ci/runner_spec.rb"
+- "./spec/requests/api/commits_spec.rb"
- "./spec/requests/api/graphql/mutations/ci/pipeline_destroy_spec.rb"
-- "./spec/requests/api/graphql/project/issues_spec.rb"
-- "./spec/requests/api/graphql/project/merge_request_spec.rb"
-- "./spec/requests/api/graphql/project_query_spec.rb"
-- "./spec/requests/api/issues/issues_spec.rb"
-- "./spec/requests/api/merge_requests_spec.rb"
-- "./spec/requests/api/projects_spec.rb"
- "./spec/requests/api/resource_access_tokens_spec.rb"
- "./spec/requests/api/users_spec.rb"
-- "./spec/requests/lfs_http_spec.rb"
-- "./spec/requests/projects/cycle_analytics_events_spec.rb"
-- "./spec/serializers/ci/downloadable_artifact_entity_spec.rb"
-- "./spec/serializers/ci/downloadable_artifact_serializer_spec.rb"
-- "./spec/serializers/ci/pipeline_entity_spec.rb"
-- "./spec/serializers/merge_request_poll_cached_widget_entity_spec.rb"
-- "./spec/serializers/merge_request_poll_widget_entity_spec.rb"
-- "./spec/serializers/merge_request_widget_entity_spec.rb"
-- "./spec/serializers/pipeline_details_entity_spec.rb"
-- "./spec/serializers/pipeline_serializer_spec.rb"
-- "./spec/serializers/stage_entity_spec.rb"
-- "./spec/serializers/stage_serializer_spec.rb"
-- "./spec/serializers/test_report_entity_spec.rb"
-- "./spec/serializers/test_report_summary_entity_spec.rb"
-- "./spec/serializers/test_suite_entity_spec.rb"
-- "./spec/serializers/test_suite_summary_entity_spec.rb"
-- "./spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb"
-- "./spec/services/ci/compare_accessibility_reports_service_spec.rb"
-- "./spec/services/ci/compare_codequality_reports_service_spec.rb"
-- "./spec/services/ci/compare_reports_base_service_spec.rb"
-- "./spec/services/ci/compare_test_reports_service_spec.rb"
- "./spec/services/ci/create_pipeline_service/environment_spec.rb"
- "./spec/services/ci/create_pipeline_service_spec.rb"
- "./spec/services/ci/destroy_pipeline_service_spec.rb"
-- "./spec/services/ci/disable_user_pipeline_schedules_service_spec.rb"
- "./spec/services/ci/ensure_stage_service_spec.rb"
- "./spec/services/ci/expire_pipeline_cache_service_spec.rb"
-- "./spec/services/ci/generate_codequality_mr_diff_report_service_spec.rb"
-- "./spec/services/ci/generate_coverage_reports_service_spec.rb"
- "./spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb"
- "./spec/services/ci/job_artifacts/destroy_associations_service_spec.rb"
-- "./spec/services/ci/job_artifacts/destroy_batch_service_spec.rb"
-- "./spec/services/ci/pipeline_artifacts/coverage_report_service_spec.rb"
-- "./spec/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service_spec.rb"
- "./spec/services/ci/pipeline_bridge_status_service_spec.rb"
-- "./spec/services/ci/pipeline_processing/shared_processing_service.rb"
- "./spec/services/ci/pipelines/add_job_service_spec.rb"
-- "./spec/services/ci/pipeline_schedule_service_spec.rb"
-- "./spec/services/ci/pipeline_trigger_service_spec.rb"
-- "./spec/services/ci/register_job_service_spec.rb"
- "./spec/services/ci/retry_build_service_spec.rb"
-- "./spec/services/ci/test_failure_history_service_spec.rb"
-- "./spec/services/ci/update_instance_variables_service_spec.rb"
-- "./spec/services/deployments/update_environment_service_spec.rb"
-- "./spec/services/design_management/save_designs_service_spec.rb"
-- "./spec/services/environments/stop_service_spec.rb"
- "./spec/services/groups/transfer_service_spec.rb"
-- "./spec/services/integrations/test/project_service_spec.rb"
-- "./spec/services/issuable/destroy_service_spec.rb"
-- "./spec/services/issue_links/list_service_spec.rb"
-- "./spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb"
-- "./spec/services/merge_requests/mergeability_check_service_spec.rb"
-- "./spec/services/merge_requests/post_merge_service_spec.rb"
-- "./spec/services/merge_requests/refresh_service_spec.rb"
-- "./spec/services/pages/migrate_from_legacy_storage_service_spec.rb"
- "./spec/services/projects/destroy_service_spec.rb"
+- "./spec/services/projects/overwrite_project_service_spec.rb"
- "./spec/services/projects/transfer_service_spec.rb"
-- "./spec/services/projects/update_service_spec.rb"
-- "./spec/services/releases/create_service_spec.rb"
- "./spec/services/resource_access_tokens/revoke_service_spec.rb"
-- "./spec/services/todo_service_spec.rb"
-- "./spec/services/users/activity_service_spec.rb"
- "./spec/services/users/destroy_service_spec.rb"
- "./spec/services/users/reject_service_spec.rb"
-- "./spec/support/shared_contexts/email_shared_context.rb"
-- "./spec/support/shared_examples/controllers/access_tokens_controller_shared_examples.rb"
-- "./spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb"
-- "./spec/support/shared_examples/integrations/test_examples.rb"
-- "./spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb"
-- "./spec/support/shared_examples/models/cluster_application_status_shared_examples.rb"
-- "./spec/support/shared_examples/models/cluster_application_version_shared_examples.rb"
-- "./spec/support/shared_examples/models/concerns/cron_schedulable_shared_examples.rb"
-- "./spec/support/shared_examples/models/concerns/limitable_shared_examples.rb"
-- "./spec/support/shared_examples/models/update_highest_role_shared_examples.rb"
-- "./spec/support/shared_examples/models/update_project_statistics_shared_examples.rb"
-- "./spec/support/shared_examples/models/with_uploads_shared_examples.rb"
-- "./spec/support/shared_examples/requests/api/status_shared_examples.rb"
-- "./spec/support/shared_examples/requests/lfs_http_shared_examples.rb"
-- "./spec/support/shared_examples/services/destroy_label_links_shared_examples.rb"
-- "./spec/support/shared_examples/services/issuable/destroy_service_shared_examples.rb"
-- "./spec/support/shared_examples/services/notification_service_shared_examples.rb"
-- "./spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb"
-- "./spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb"
-- "./spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb"
-- "./spec/support/shared_examples/workers/idempotency_shared_examples.rb"
-- "./spec/views/projects/artifacts/_artifact.html.haml_spec.rb"
-- "./spec/views/projects/commits/_commit.html.haml_spec.rb"
-- "./spec/views/projects/jobs/_build.html.haml_spec.rb"
-- "./spec/views/projects/jobs/_generic_commit_status.html.haml_spec.rb"
-- "./spec/views/projects/merge_requests/creations/_new_submit.html.haml_spec.rb"
-- "./spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb"
-- "./spec/views/shared/runners/_runner_details.html.haml_spec.rb"
-- "./spec/workers/authorized_project_update/user_refresh_from_replica_worker_spec.rb"
-- "./spec/workers/ci/pipeline_artifacts/create_quality_report_worker_spec.rb"
-- "./spec/workers/container_expiration_policy_worker_spec.rb"
- "./spec/workers/merge_requests/create_pipeline_worker_spec.rb"
-- "./spec/workers/pipeline_metrics_worker_spec.rb"
-- "./spec/workers/pipeline_schedule_worker_spec.rb"
-- "./spec/workers/releases/create_evidence_worker_spec.rb"
- "./spec/workers/remove_expired_members_worker_spec.rb"
- "./spec/workers/repository_cleanup_worker_spec.rb"
-- "./spec/workers/stage_update_worker_spec.rb"
-- "./spec/workers/stuck_merge_jobs_worker_spec.rb"
-- "./ee/spec/requests/api/graphql/project/pipelines/dast_profile_spec.rb"
-- "./spec/services/projects/overwrite_project_service_spec.rb"
diff --git a/spec/support/database/cross-join-allowlist.yml b/spec/support/database/cross-join-allowlist.yml
index c209d275fc8..19b1ce30d5f 100644
--- a/spec/support/database/cross-join-allowlist.yml
+++ b/spec/support/database/cross-join-allowlist.yml
@@ -1,58 +1,6 @@
-- "./ee/spec/features/ci/ci_minutes_spec.rb"
-- "./ee/spec/features/merge_trains/two_merge_requests_on_train_spec.rb"
-- "./ee/spec/features/merge_trains/user_adds_merge_request_to_merge_train_spec.rb"
-- "./ee/spec/finders/ee/namespaces/projects_finder_spec.rb"
-- "./ee/spec/graphql/ee/resolvers/namespace_projects_resolver_spec.rb"
-- "./ee/spec/models/ci/minutes/project_monthly_usage_spec.rb"
-- "./ee/spec/models/project_spec.rb"
-- "./ee/spec/models/security/finding_spec.rb"
-- "./ee/spec/models/security/scan_spec.rb"
-- "./ee/spec/requests/api/ci/minutes_spec.rb"
-- "./ee/spec/requests/api/graphql/ci/minutes/usage_spec.rb"
-- "./ee/spec/requests/api/namespaces_spec.rb"
-- "./ee/spec/services/ci/minutes/additional_packs/change_namespace_service_spec.rb"
-- "./ee/spec/services/ci/minutes/additional_packs/create_service_spec.rb"
-- "./ee/spec/services/ci/minutes/refresh_cached_data_service_spec.rb"
-- "./spec/controllers/admin/runners_controller_spec.rb"
-- "./spec/controllers/groups/settings/ci_cd_controller_spec.rb"
-- "./spec/controllers/projects/settings/ci_cd_controller_spec.rb"
-- "./spec/features/admin/admin_runners_spec.rb"
-- "./spec/features/ide/user_opens_merge_request_spec.rb"
-- "./spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb"
-- "./spec/features/projects/infrastructure_registry_spec.rb"
-- "./spec/finders/ci/pipelines_for_merge_request_finder_spec.rb"
-- "./spec/finders/ci/runners_finder_spec.rb"
-- "./spec/frontend/fixtures/runner.rb"
-- "./spec/graphql/resolvers/ci/group_runners_resolver_spec.rb"
-- "./spec/lib/api/entities/package_spec.rb"
- "./spec/lib/gitlab/background_migration/copy_ci_builds_columns_to_security_scans_spec.rb"
- "./spec/lib/gitlab/background_migration/migrate_pages_metadata_spec.rb"
- "./spec/migrations/20210907211557_finalize_ci_builds_bigint_conversion_spec.rb"
- "./spec/migrations/associate_existing_dast_builds_with_variables_spec.rb"
+- "./spec/migrations/disable_job_token_scope_when_unused_spec.rb"
- "./spec/migrations/schedule_copy_ci_builds_columns_to_security_scans2_spec.rb"
-- "./spec/migrations/schedule_pages_metadata_migration_spec.rb"
-- "./spec/models/ci/pipeline_spec.rb"
-- "./spec/models/ci/runner_spec.rb"
-- "./spec/models/merge_request_spec.rb"
-- "./spec/models/project_spec.rb"
-- "./spec/models/user_spec.rb"
-- "./spec/presenters/packages/detail/package_presenter_spec.rb"
-- "./spec/requests/api/ci/runner/runners_post_spec.rb"
-- "./spec/requests/api/ci/runners_spec.rb"
-- "./spec/requests/api/graphql/ci/runner_spec.rb"
-- "./spec/requests/api/graphql/group_query_spec.rb"
-- "./spec/requests/api/graphql/packages/composer_spec.rb"
-- "./spec/requests/api/graphql/packages/conan_spec.rb"
-- "./spec/requests/api/graphql/packages/maven_spec.rb"
-- "./spec/requests/api/graphql/packages/nuget_spec.rb"
-- "./spec/requests/api/graphql/packages/package_spec.rb"
-- "./spec/requests/api/graphql/packages/pypi_spec.rb"
-- "./spec/requests/api/package_files_spec.rb"
-- "./spec/services/environments/stop_service_spec.rb"
-- "./spec/services/merge_requests/post_merge_service_spec.rb"
-- "./spec/support/shared_examples/features/packages_shared_examples.rb"
-- "./spec/support/shared_examples/models/concerns/limitable_shared_examples.rb"
-- "./spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb"
-- "./spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb"
-- "./spec/support/shared_examples/requests/graphql_shared_examples.rb"
-- "./spec/support/shared_examples/services/packages_shared_examples.rb"
diff --git a/spec/support/database/gitlab_schema.rb b/spec/support/database/gitlab_schema.rb
deleted file mode 100644
index fe05fb998e6..00000000000
--- a/spec/support/database/gitlab_schema.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-# This module gathes information about table to schema mapping
-# to understand table affinity
-module Database
- module GitlabSchema
- def self.table_schemas(tables)
- tables.map { |table| table_schema(table) }.to_set
- end
-
- def self.table_schema(name)
- tables_to_schema[name] || :undefined
- end
-
- def self.tables_to_schema
- @tables_to_schema ||= all_classes_with_schema.to_h do |klass|
- [klass.table_name, klass.gitlab_schema]
- end
- end
-
- def self.all_classes_with_schema
- ActiveRecord::Base.descendants.reject(&:abstract_class?).select(&:gitlab_schema?) # rubocop:disable Database/MultipleDatabases
- end
- end
-end
diff --git a/spec/support/database/multiple_databases.rb b/spec/support/database/multiple_databases.rb
index 5e1ae60536f..9e72ea589e3 100644
--- a/spec/support/database/multiple_databases.rb
+++ b/spec/support/database/multiple_databases.rb
@@ -6,6 +6,18 @@ module Database
skip 'Skipping because multiple databases not set up' unless Gitlab::Database.has_config?(:ci)
end
+ def reconfigure_db_connection(name: nil, config_hash: {}, model: ActiveRecord::Base, config_model: nil)
+ db_config = (config_model || model).connection_db_config
+
+ new_db_config = ActiveRecord::DatabaseConfigurations::HashConfig.new(
+ db_config.env_name,
+ name ? name.to_s : db_config.name,
+ db_config.configuration_hash.merge(config_hash)
+ )
+
+ model.establish_connection(new_db_config)
+ end
+
# The usage of this method switches temporarily used `connection_handler`
# allowing full manipulation of ActiveRecord::Base connections without
# having side effects like:
@@ -56,6 +68,21 @@ RSpec.configure do |config|
example.run
end
end
+
+ config.around(:each, :mocked_ci_connection) do |example|
+ with_reestablished_active_record_base(reconnect: true) do
+ reconfigure_db_connection(
+ name: :ci,
+ model: Ci::ApplicationRecord,
+ config_model: ActiveRecord::Base
+ )
+
+ example.run
+
+ # Cleanup connection_specification_name for Ci::ApplicationRecord
+ Ci::ApplicationRecord.remove_connection
+ end
+ end
end
ActiveRecord::Base.singleton_class.prepend(::Database::ActiveRecordBaseEstablishConnection) # rubocop:disable Database/MultipleDatabases
diff --git a/spec/support/database/prevent_cross_database_modification.rb b/spec/support/database/prevent_cross_database_modification.rb
index 7ded85b65ce..c509aecf9b8 100644
--- a/spec/support/database/prevent_cross_database_modification.rb
+++ b/spec/support/database/prevent_cross_database_modification.rb
@@ -1,123 +1,31 @@
# frozen_string_literal: true
-module Database
- module PreventCrossDatabaseModification
- CrossDatabaseModificationAcrossUnsupportedTablesError = Class.new(StandardError)
-
- module GitlabDatabaseMixin
- def allow_cross_database_modification_within_transaction(url:)
- cross_database_context = Database::PreventCrossDatabaseModification.cross_database_context
- return yield unless cross_database_context && cross_database_context[:enabled]
-
- transaction_tracker_enabled_was = cross_database_context[:enabled]
- cross_database_context[:enabled] = false
-
- yield
- ensure
- cross_database_context[:enabled] = transaction_tracker_enabled_was if cross_database_context
- end
- end
-
- module SpecHelpers
- def with_cross_database_modification_prevented
- subscriber = ActiveSupport::Notifications.subscribe('sql.active_record') do |name, start, finish, id, payload|
- PreventCrossDatabaseModification.prevent_cross_database_modification!(payload[:connection], payload[:sql])
- end
-
- PreventCrossDatabaseModification.reset_cross_database_context!
- PreventCrossDatabaseModification.cross_database_context.merge!(enabled: true, subscriber: subscriber)
-
- yield if block_given?
- ensure
- cleanup_with_cross_database_modification_prevented if block_given?
- end
-
- def cleanup_with_cross_database_modification_prevented
- if PreventCrossDatabaseModification.cross_database_context
- ActiveSupport::Notifications.unsubscribe(PreventCrossDatabaseModification.cross_database_context[:subscriber])
- PreventCrossDatabaseModification.cross_database_context[:enabled] = false
- end
- end
- end
-
- def self.cross_database_context
- Thread.current[:transaction_tracker]
- end
-
- def self.reset_cross_database_context!
- Thread.current[:transaction_tracker] = initial_data
- end
-
- def self.initial_data
- {
- enabled: false,
- transaction_depth_by_db: Hash.new { |h, k| h[k] = 0 },
- modified_tables_by_db: Hash.new { |h, k| h[k] = Set.new }
- }
- end
-
- def self.prevent_cross_database_modification!(connection, sql)
- return unless cross_database_context
- return unless cross_database_context[:enabled]
-
- return if connection.pool.instance_of?(ActiveRecord::ConnectionAdapters::NullPool)
-
- database = connection.pool.db_config.name
-
- if sql.start_with?('SAVEPOINT')
- cross_database_context[:transaction_depth_by_db][database] += 1
-
- return
- elsif sql.start_with?('RELEASE SAVEPOINT', 'ROLLBACK TO SAVEPOINT')
- cross_database_context[:transaction_depth_by_db][database] -= 1
- if cross_database_context[:transaction_depth_by_db][database] <= 0
- cross_database_context[:modified_tables_by_db][database].clear
- end
-
- return
- end
-
- return if cross_database_context[:transaction_depth_by_db].values.all?(&:zero?)
-
- # PgQuery might fail in some cases due to limited nesting:
- # https://github.com/pganalyze/pg_query/issues/209
- parsed_query = PgQuery.parse(sql)
- tables = sql.downcase.include?(' for update') ? parsed_query.tables : parsed_query.dml_tables
-
- return if tables.empty?
-
- cross_database_context[:modified_tables_by_db][database].merge(tables)
-
- all_tables = cross_database_context[:modified_tables_by_db].values.map(&:to_a).flatten
- schemas = Database::GitlabSchema.table_schemas(all_tables)
-
- if schemas.many?
- raise Database::PreventCrossDatabaseModification::CrossDatabaseModificationAcrossUnsupportedTablesError,
- "Cross-database data modification of '#{schemas.to_a.join(", ")}' were detected within " \
- "a transaction modifying the '#{all_tables.to_a.join(", ")}' tables." \
- "Please refer to https://docs.gitlab.com/ee/development/database/multiple_databases.html#removing-cross-database-transactions for details on how to resolve this exception."
- end
- end
- end
+module PreventCrossDatabaseModificationSpecHelpers
+ delegate :with_cross_database_modification_prevented,
+ :allow_cross_database_modification_within_transaction,
+ to: :'::Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification'
end
-Gitlab::Database.singleton_class.prepend(
- Database::PreventCrossDatabaseModification::GitlabDatabaseMixin)
-
CROSS_DB_MODIFICATION_ALLOW_LIST = Set.new(YAML.load_file(File.join(__dir__, 'cross-database-modification-allowlist.yml'))).freeze
RSpec.configure do |config|
- config.include(::Database::PreventCrossDatabaseModification::SpecHelpers)
+ config.include(PreventCrossDatabaseModificationSpecHelpers)
+
+ # By default allow cross-modifications as we want to observe only transactions
+ # within a specific block of execution which is defined be `before(:each)` and `after(:each)`
+ config.before(:all) do
+ ::Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.suppress = true
+ end
# Using before and after blocks because the around block causes problems with the let_it_be
# record creations. It makes an extra savepoint which breaks the transaction count logic.
config.before do |example_file|
- if CROSS_DB_MODIFICATION_ALLOW_LIST.exclude?(example_file.file_path)
- with_cross_database_modification_prevented
- end
+ ::Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.suppress =
+ CROSS_DB_MODIFICATION_ALLOW_LIST.include?(example_file.file_path_rerun_argument)
end
+ # Reset after execution to preferred state
config.after do |example_file|
- cleanup_with_cross_database_modification_prevented
+ ::Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.suppress = true
end
end
diff --git a/spec/support/database/prevent_cross_joins.rb b/spec/support/database/prevent_cross_joins.rb
index f5ed2a8f22e..e69374fbc70 100644
--- a/spec/support/database/prevent_cross_joins.rb
+++ b/spec/support/database/prevent_cross_joins.rb
@@ -35,7 +35,7 @@ module Database
# https://github.com/pganalyze/pg_query/issues/209
tables = PgQuery.parse(sql).tables
- schemas = Database::GitlabSchema.table_schemas(tables)
+ schemas = ::Gitlab::Database::GitlabSchema.table_schemas(tables)
if schemas.include?(:gitlab_ci) && schemas.include?(:gitlab_main)
Thread.current[:has_cross_join_exception] = true
@@ -96,7 +96,7 @@ RSpec.configure do |config|
config.around do |example|
Thread.current[:has_cross_join_exception] = false
- if ALLOW_LIST.include?(example.file_path)
+ if ALLOW_LIST.include?(example.file_path_rerun_argument)
example.run
else
with_cross_joins_prevented { example.run }
diff --git a/spec/support/database/query_analyzer.rb b/spec/support/database/query_analyzer.rb
new file mode 100644
index 00000000000..85fa55f81ef
--- /dev/null
+++ b/spec/support/database/query_analyzer.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+# With the usage of `describe '...', query_analyzers: false`
+# can be disabled selectively
+
+RSpec.configure do |config|
+ config.around do |example|
+ if example.metadata.fetch(:query_analyzers, true)
+ ::Gitlab::Database::QueryAnalyzer.instance.within { example.run }
+ else
+ example.run
+ end
+ end
+end
diff --git a/spec/support/database_load_balancing.rb b/spec/support/database_load_balancing.rb
index 014575e8a82..d2902ddcc7c 100644
--- a/spec/support/database_load_balancing.rb
+++ b/spec/support/database_load_balancing.rb
@@ -2,17 +2,17 @@
RSpec.configure do |config|
config.around(:each, :database_replica) do |example|
- old_proxies = []
+ old_proxies = {}
Gitlab::Database::LoadBalancing.base_models.each do |model|
+ old_proxies[model] = [model.load_balancer, model.connection, model.sticking]
+
config = Gitlab::Database::LoadBalancing::Configuration
.new(model, [model.connection_db_config.configuration_hash[:host]])
- lb = Gitlab::Database::LoadBalancing::LoadBalancer.new(config)
-
- old_proxies << [model, model.connection]
- model.connection =
- Gitlab::Database::LoadBalancing::ConnectionProxy.new(lb)
+ model.load_balancer = Gitlab::Database::LoadBalancing::LoadBalancer.new(config)
+ model.sticking = Gitlab::Database::LoadBalancing::Sticking.new(model.load_balancer)
+ model.connection = Gitlab::Database::LoadBalancing::ConnectionProxy.new(model.load_balancer)
end
Gitlab::Database::LoadBalancing::Session.clear_session
@@ -23,8 +23,8 @@ RSpec.configure do |config|
Gitlab::Database::LoadBalancing::Session.clear_session
redis_shared_state_cleanup!
- old_proxies.each do |(model, proxy)|
- model.connection = proxy
+ old_proxies.each do |model, proxy|
+ model.load_balancer, model.connection, model.sticking = proxy
end
end
end
diff --git a/spec/support/flaky_tests.rb b/spec/support/flaky_tests.rb
new file mode 100644
index 00000000000..30a064d8705
--- /dev/null
+++ b/spec/support/flaky_tests.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+return unless ENV['CI']
+return unless ENV['SKIP_FLAKY_TESTS_AUTOMATICALLY'] == "true"
+return if ENV['CI_MERGE_REQUEST_LABELS'].to_s.include?('pipeline:run-flaky-tests')
+
+require_relative '../../tooling/rspec_flaky/report'
+
+RSpec.configure do |config|
+ $flaky_test_example_ids = begin # rubocop:disable Style/GlobalVars
+ raise "$SUITE_FLAKY_RSPEC_REPORT_PATH is empty." if ENV['SUITE_FLAKY_RSPEC_REPORT_PATH'].to_s.empty?
+ raise "#{ENV['SUITE_FLAKY_RSPEC_REPORT_PATH']} doesn't exist" unless File.exist?(ENV['SUITE_FLAKY_RSPEC_REPORT_PATH'])
+
+ RspecFlaky::Report.load(ENV['SUITE_FLAKY_RSPEC_REPORT_PATH']).map { |_, flaky_test_data| flaky_test_data["example_id"] }
+ rescue => e # rubocop:disable Style/RescueStandardError
+ puts e
+ []
+ end
+ $skipped_flaky_tests_report = [] # rubocop:disable Style/GlobalVars
+
+ config.around do |example|
+ # Skip flaky tests automatically
+ if $flaky_test_example_ids.include?(example.id) # rubocop:disable Style/GlobalVars
+ puts "Skipping #{example.id} '#{example.full_description}' because it's flaky."
+ $skipped_flaky_tests_report << example.id # rubocop:disable Style/GlobalVars
+ else
+ example.run
+ end
+ end
+
+ config.after(:suite) do
+ next unless ENV['SKIPPED_FLAKY_TESTS_REPORT_PATH']
+
+ File.write(ENV['SKIPPED_FLAKY_TESTS_REPORT_PATH'], "#{$skipped_flaky_tests_report.join("\n")}\n") # rubocop:disable Style/GlobalVars
+ end
+end
diff --git a/spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb b/spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb
index de9735df546..4624a8ac82a 100644
--- a/spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb
+++ b/spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb
@@ -6,7 +6,9 @@ RSpec.shared_examples 'a correct instrumented metric value' do |params|
let(:metric) { described_class.new(time_frame: time_frame, options: options) }
before do
- allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
+ if described_class.respond_to?(:relation) && described_class.relation.respond_to?(:connection)
+ allow(described_class.relation.connection).to receive(:transaction_open?).and_return(false)
+ end
end
it 'has correct value' do
diff --git a/spec/support/graphql/fake_query_type.rb b/spec/support/graphql/fake_query_type.rb
new file mode 100644
index 00000000000..ffd851a6e6a
--- /dev/null
+++ b/spec/support/graphql/fake_query_type.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Graphql
+ class FakeQueryType < Types::BaseObject
+ graphql_name 'FakeQuery'
+
+ field :hello_world, String, null: true do
+ argument :message, String, required: false
+ end
+
+ def hello_world(message: "world")
+ "Hello #{message}!"
+ end
+ end
+end
diff --git a/spec/support/graphql/fake_tracer.rb b/spec/support/graphql/fake_tracer.rb
new file mode 100644
index 00000000000..c2fb7ed12d8
--- /dev/null
+++ b/spec/support/graphql/fake_tracer.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Graphql
+ class FakeTracer
+ def initialize(trace_callback)
+ @trace_callback = trace_callback
+ end
+
+ def trace(*args)
+ @trace_callback.call(*args)
+
+ yield
+ end
+ end
+end
diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb
index 3ec52f8c832..722d484609c 100644
--- a/spec/support/helpers/cycle_analytics_helpers.rb
+++ b/spec/support/helpers/cycle_analytics_helpers.rb
@@ -63,6 +63,10 @@ module CycleAnalyticsHelpers
wait_for_requests
end
+ def click_save_value_stream_button
+ click_button(_('Save value stream'))
+ end
+
def create_custom_value_stream(custom_value_stream_name)
toggle_value_stream_dropdown
page.find_button(_('Create new Value Stream')).click
diff --git a/spec/support/helpers/features/invite_members_modal_helper.rb b/spec/support/helpers/features/invite_members_modal_helper.rb
index 69ba20c1ca4..3502558b2c2 100644
--- a/spec/support/helpers/features/invite_members_modal_helper.rb
+++ b/spec/support/helpers/features/invite_members_modal_helper.rb
@@ -8,7 +8,7 @@ module Spec
def invite_member(name, role: 'Guest', expires_at: nil, area_of_focus: false)
click_on 'Invite members'
- page.within '#invite-members-modal' do
+ page.within '[data-testid="invite-members-modal"]' do
find('[data-testid="members-token-select-input"]').set(name)
wait_for_requests
diff --git a/spec/support/helpers/gitaly_setup.rb b/spec/support/helpers/gitaly_setup.rb
index 5cfd03ecea8..8a329c2f9dd 100644
--- a/spec/support/helpers/gitaly_setup.rb
+++ b/spec/support/helpers/gitaly_setup.rb
@@ -98,7 +98,7 @@ module GitalySetup
end
def build_gitaly
- system(env, 'make', chdir: tmp_tests_gitaly_dir) # rubocop:disable GitlabSecurity/SystemCommandInjection
+ system(env.merge({ 'GIT_VERSION' => nil }), 'make all git', chdir: tmp_tests_gitaly_dir) # rubocop:disable GitlabSecurity/SystemCommandInjection
end
def start_gitaly
diff --git a/spec/support/helpers/gpg_helpers.rb b/spec/support/helpers/gpg_helpers.rb
index 813c6176317..81e669aab57 100644
--- a/spec/support/helpers/gpg_helpers.rb
+++ b/spec/support/helpers/gpg_helpers.rb
@@ -4,6 +4,7 @@ module GpgHelpers
SIGNED_COMMIT_SHA = '8a852d50dda17cc8fd1408d2fd0c5b0f24c76ca4'
SIGNED_AND_AUTHORED_SHA = '3c1d9a0266cb0c62d926f4a6c649beed561846f5'
DIFFERING_EMAIL_SHA = 'a17a9f66543673edf0a3d1c6b93bdda3fe600f32'
+ MULTIPLE_SIGNATURES_SHA = 'c7794c14268d67ad8a2d5f066d706539afc75a96'
module User1
extend self
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index 6f17d3cb496..ee4621deb2d 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -522,8 +522,7 @@ module GraphqlHelpers
end
end
- # See note at graphql_data about memoization and multiple requests
- def graphql_errors(body = json_response)
+ def graphql_errors(body = fresh_response_data)
case body
when Hash # regular query
body['errors']
diff --git a/spec/support/helpers/migrations_helpers.rb b/spec/support/helpers/migrations_helpers.rb
index 7799e49d4c1..0c5bf09f6b7 100644
--- a/spec/support/helpers/migrations_helpers.rb
+++ b/spec/support/helpers/migrations_helpers.rb
@@ -2,7 +2,7 @@
module MigrationsHelpers
def active_record_base
- ActiveRecord::Base
+ Gitlab::Database.database_base_models.fetch(self.class.metadata[:database] || :main)
end
def table(name)
@@ -34,7 +34,7 @@ module MigrationsHelpers
end
def migrations_paths
- ActiveRecord::Migrator.migrations_paths
+ active_record_base.connection.migrations_paths
end
def migration_context
@@ -52,7 +52,7 @@ module MigrationsHelpers
end
def foreign_key_exists?(source, target = nil, column: nil)
- ActiveRecord::Base.connection.foreign_keys(source).any? do |key|
+ active_record_base.connection.foreign_keys(source).any? do |key|
if column
key.options[:column].to_s == column.to_s
else
diff --git a/spec/support/helpers/navbar_structure_helper.rb b/spec/support/helpers/navbar_structure_helper.rb
index 96e79427278..c2ec82155cd 100644
--- a/spec/support/helpers/navbar_structure_helper.rb
+++ b/spec/support/helpers/navbar_structure_helper.rb
@@ -29,6 +29,19 @@ module NavbarStructureHelper
)
end
+ def insert_customer_relations_nav(within)
+ insert_after_nav_item(
+ within,
+ new_nav_item: {
+ nav_item: _('Customer relations'),
+ nav_sub_items: [
+ _('Contacts'),
+ _('Organizations')
+ ]
+ }
+ )
+ end
+
def insert_container_nav
insert_after_sub_nav_item(
_('Package Registry'),
diff --git a/spec/support/helpers/project_forks_helper.rb b/spec/support/helpers/project_forks_helper.rb
index 4b4285f251e..84b5dbc1d23 100644
--- a/spec/support/helpers/project_forks_helper.rb
+++ b/spec/support/helpers/project_forks_helper.rb
@@ -28,11 +28,15 @@ module ProjectForksHelper
unless params[:target_project] || params[:using_service]
target_level = [project.visibility_level, namespace.visibility_level].min
visibility_level = Gitlab::VisibilityLevel.closest_allowed_level(target_level)
+ # Builds and MRs can't have higher visibility level than repository access level.
+ builds_access_level = [project.builds_access_level, project.repository_access_level].min
params[:target_project] =
create(:project,
(:repository if create_repository),
- visibility_level: visibility_level, creator: user, namespace: namespace)
+ visibility_level: visibility_level,
+ builds_access_level: builds_access_level,
+ creator: user, namespace: namespace)
end
service = Projects::ForkService.new(project, user, params)
diff --git a/spec/support/helpers/require_migration.rb b/spec/support/helpers/require_migration.rb
index de3a8a81ab5..ee28f8e504c 100644
--- a/spec/support/helpers/require_migration.rb
+++ b/spec/support/helpers/require_migration.rb
@@ -15,7 +15,7 @@ class RequireMigration
end
MIGRATION_FOLDERS = %w[db/migrate db/post_migrate].freeze
- SPEC_FILE_PATTERN = %r{.+/(?<file_name>.+)_spec\.rb}.freeze
+ SPEC_FILE_PATTERN = %r{.+/(?:\d+_)?(?<file_name>.+)_spec\.rb}.freeze
class << self
def require_migration!(file_name)
@@ -26,10 +26,12 @@ class RequireMigration
end
def search_migration_file(file_name)
+ migration_file_pattern = /\A\d+_#{file_name}\.rb\z/
+
migration_folders.flat_map do |path|
migration_path = Rails.root.join(path).to_s
- Find.find(migration_path).select { |m| File.basename(m).match? /\A\d+_#{file_name}\.rb\z/ }
+ Find.find(migration_path).select { |m| migration_file_pattern.match? File.basename(m) }
end
end
diff --git a/spec/support/helpers/stub_gitlab_calls.rb b/spec/support/helpers/stub_gitlab_calls.rb
index 6f530d57caf..ef3c39c83c2 100644
--- a/spec/support/helpers/stub_gitlab_calls.rb
+++ b/spec/support/helpers/stub_gitlab_calls.rb
@@ -92,9 +92,16 @@ module StubGitlabCalls
end
def stub_commonmark_sourcepos_disabled
+ render_options =
+ if Feature.enabled?(:use_cmark_renderer)
+ Banzai::Filter::MarkdownEngines::CommonMark::RENDER_OPTIONS_C
+ else
+ Banzai::Filter::MarkdownEngines::CommonMark::RENDER_OPTIONS_RUBY
+ end
+
allow_any_instance_of(Banzai::Filter::MarkdownEngines::CommonMark)
.to receive(:render_options)
- .and_return(Banzai::Filter::MarkdownEngines::CommonMark::RENDER_OPTIONS)
+ .and_return(render_options)
end
private
diff --git a/spec/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb
index 56177d445d6..5e86b08aa45 100644
--- a/spec/support/helpers/stub_object_storage.rb
+++ b/spec/support/helpers/stub_object_storage.rb
@@ -4,7 +4,6 @@ module StubObjectStorage
def stub_dependency_proxy_object_storage(**params)
stub_object_storage_uploader(config: ::Gitlab.config.dependency_proxy.object_store,
uploader: ::DependencyProxy::FileUploader,
- remote_directory: 'dependency_proxy',
**params)
end
@@ -16,7 +15,6 @@ module StubObjectStorage
def stub_object_storage_uploader(
config:,
uploader:,
- remote_directory:,
enabled: true,
proxy_download: false,
background_upload: false,
@@ -40,7 +38,7 @@ module StubObjectStorage
return unless enabled
stub_object_storage(connection_params: uploader.object_store_credentials,
- remote_directory: remote_directory)
+ remote_directory: config.remote_directory)
end
def stub_object_storage(connection_params:, remote_directory:)
@@ -60,56 +58,48 @@ module StubObjectStorage
def stub_artifacts_object_storage(uploader = JobArtifactUploader, **params)
stub_object_storage_uploader(config: Gitlab.config.artifacts.object_store,
uploader: uploader,
- remote_directory: 'artifacts',
**params)
end
def stub_external_diffs_object_storage(uploader = described_class, **params)
stub_object_storage_uploader(config: Gitlab.config.external_diffs.object_store,
uploader: uploader,
- remote_directory: 'external-diffs',
**params)
end
def stub_lfs_object_storage(**params)
stub_object_storage_uploader(config: Gitlab.config.lfs.object_store,
uploader: LfsObjectUploader,
- remote_directory: 'lfs-objects',
**params)
end
def stub_package_file_object_storage(**params)
stub_object_storage_uploader(config: Gitlab.config.packages.object_store,
uploader: ::Packages::PackageFileUploader,
- remote_directory: 'packages',
**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,
- remote_directory: 'uploads',
**params)
end
def stub_terraform_state_object_storage(**params)
stub_object_storage_uploader(config: Gitlab.config.terraform_state.object_store,
uploader: Terraform::StateUploader,
- remote_directory: 'terraform',
**params)
end
def stub_pages_object_storage(uploader = described_class, **params)
stub_object_storage_uploader(config: Gitlab.config.pages.object_store,
uploader: uploader,
- remote_directory: 'pages',
**params)
end
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index badd4e8212c..acbc15f7b62 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -9,7 +9,7 @@ module TestEnv
# When developing the seed repository, comment out the branch you will modify.
BRANCH_SHA = {
- 'signed-commits' => '6101e87',
+ 'signed-commits' => 'c7794c1',
'not-merged-branch' => 'b83d6e3',
'branch-merged' => '498214d',
'empty-branch' => '7efb185',
@@ -53,7 +53,7 @@ module TestEnv
'wip' => 'b9238ee',
'csv' => '3dd0896',
'v1.1.0' => 'b83d6e3',
- 'add-ipython-files' => 'f6b7a70',
+ 'add-ipython-files' => '2b5ef814',
'add-pdf-file' => 'e774ebd',
'squash-large-files' => '54cec52',
'add-pdf-text-binary' => '79faa7b',
diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb
index 5ead1813439..5865bafd382 100644
--- a/spec/support/helpers/usage_data_helpers.rb
+++ b/spec/support/helpers/usage_data_helpers.rb
@@ -162,6 +162,8 @@ module UsageDataHelpers
def stub_usage_data_connections
allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
+ allow(::Ci::ApplicationRecord.connection).to receive(:transaction_open?).and_return(false) if ::Ci::ApplicationRecord.connection_class?
+
allow(Gitlab::Prometheus::Internal).to receive(:prometheus_enabled?).and_return(false)
end
diff --git a/spec/support/helpers/workhorse_helpers.rb b/spec/support/helpers/workhorse_helpers.rb
index cd8387de686..83bda6e03b1 100644
--- a/spec/support/helpers/workhorse_helpers.rb
+++ b/spec/support/helpers/workhorse_helpers.rb
@@ -24,7 +24,12 @@ module WorkhorseHelpers
# workhorse_post_with_file will transform file_key inside params as if it was disk accelerated by workhorse
def workhorse_post_with_file(url, file_key:, params:)
- workhorse_request_with_file(:post, url,
+ workhorse_form_with_file(url, method: :post, file_key: file_key, params: params)
+ end
+
+ # workhorse_form_with_file will transform file_key inside params as if it was disk accelerated by workhorse
+ def workhorse_form_with_file(url, file_key:, params:, method: :post)
+ workhorse_request_with_file(method, url,
file_key: file_key,
params: params,
env: { 'CONTENT_TYPE' => 'multipart/form-data' },
diff --git a/spec/support/matchers/access_matchers.rb b/spec/support/matchers/access_matchers.rb
index acf5fb0944f..1b460fbdbf7 100644
--- a/spec/support/matchers/access_matchers.rb
+++ b/spec/support/matchers/access_matchers.rb
@@ -52,7 +52,7 @@ module AccessMatchers
emulate_user(user, @membership)
visit(url)
- status_code == 200 && !current_path.in?([new_user_session_path, new_admin_session_path])
+ [200, 204].include?(status_code) && !current_path.in?([new_user_session_path, new_admin_session_path])
end
chain :of do |membership|
diff --git a/spec/support/matchers/project_namespace_matcher.rb b/spec/support/matchers/project_namespace_matcher.rb
new file mode 100644
index 00000000000..95aa5429679
--- /dev/null
+++ b/spec/support/matchers/project_namespace_matcher.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+RSpec::Matchers.define :be_in_sync_with_project do |project|
+ match do |project_namespace|
+ # if project is not persisted make sure we do not have a persisted project_namespace for it
+ break false if project.new_record? && project_namespace&.persisted?
+ # don't really care if project is not in sync if the project was never persisted.
+ break true if project.new_record? && !project_namespace.present?
+
+ project_namespace.present? &&
+ project.name == project_namespace.name &&
+ project.path == project_namespace.path &&
+ project.namespace == project_namespace.parent &&
+ project.visibility_level == project_namespace.visibility_level &&
+ project.shared_runners_enabled == project_namespace.shared_runners_enabled
+ end
+
+ failure_message_when_negated do |project_namespace|
+ if project.new_record? && project_namespace&.persisted?
+ "expected that a non persisted project #{project} does not have a persisted project namespace #{project_namespace}"
+ else
+ <<-MSG
+ expected that the project's attributes name, path, namespace_id, visibility_level, shared_runners_enabled
+ are in sync with the corresponding project namespace attributes
+ MSG
+ end
+ end
+end
diff --git a/spec/support/patches/rspec_example_prepended_methods.rb b/spec/support/patches/rspec_example_prepended_methods.rb
new file mode 100644
index 00000000000..ea918b1e08f
--- /dev/null
+++ b/spec/support/patches/rspec_example_prepended_methods.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module RSpec
+ module Core
+ module ExamplePrependedMethods
+ # Based on https://github.com/rspec/rspec-core/blob/d57c371ee92b16211b80ac7b0b025968438f5297/lib/rspec/core/example.rb#L96-L104,
+ # Same as location_rerun_argument but with line number
+ def file_path_rerun_argument
+ loaded_spec_files = RSpec.configuration.loaded_spec_files
+
+ RSpec::Core::Metadata.ascending(metadata) do |meta|
+ break meta[:file_path] if loaded_spec_files.include?(meta[:absolute_file_path])
+ end
+ end
+ end
+
+ module ExampleProcsyPrependedMethods
+ def file_path_rerun_argument
+ example.file_path_rerun_argument
+ end
+ end
+ end
+end
+
+RSpec::Core::Example.prepend(RSpec::Core::ExamplePrependedMethods)
+RSpec::Core::Example::Procsy.prepend(RSpec::Core::ExampleProcsyPrependedMethods)
diff --git a/spec/support/redis/redis_shared_examples.rb b/spec/support/redis/redis_shared_examples.rb
index dd916aea3e8..72b3a72f9d4 100644
--- a/spec/support/redis/redis_shared_examples.rb
+++ b/spec/support/redis/redis_shared_examples.rb
@@ -87,6 +87,43 @@ RSpec.shared_examples "redis_shared_examples" do
end
end
+ describe '.store' do
+ let(:rails_env) { 'development' }
+
+ subject { described_class.new(rails_env).store }
+
+ shared_examples 'redis store' do
+ it 'instantiates Redis::Store' do
+ is_expected.to be_a(::Redis::Store)
+ expect(subject.to_s).to eq("Redis Client connected to #{host} against DB #{redis_database}")
+ end
+
+ context 'with the namespace' do
+ let(:namespace) { 'namespace_name' }
+
+ subject { described_class.new(rails_env).store(namespace: namespace) }
+
+ it "uses specified namespace" do
+ expect(subject.to_s).to eq("Redis Client connected to #{host} against DB #{redis_database} with namespace #{namespace}")
+ end
+ end
+ end
+
+ context 'with old format' do
+ it_behaves_like 'redis store' do
+ let(:config_file_name) { config_old_format_host }
+ let(:host) { "localhost:#{redis_port}" }
+ end
+ end
+
+ context 'with new format' do
+ it_behaves_like 'redis store' do
+ let(:config_file_name) { config_new_format_host }
+ let(:host) { "development-host:#{redis_port}" }
+ end
+ end
+ end
+
describe '.params' do
subject { described_class.new(rails_env).params }
diff --git a/spec/support/retriable.rb b/spec/support/retriable.rb
new file mode 100644
index 00000000000..be4c2d62752
--- /dev/null
+++ b/spec/support/retriable.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+Retriable.configure do |config|
+ config.multiplier = 1.0
+ config.rand_factor = 0.0
+ config.base_interval = 0
+end
diff --git a/spec/support/shared_contexts/graphql/requests/packages_shared_context.rb b/spec/support/shared_contexts/graphql/requests/packages_shared_context.rb
index 645ea742f07..9ac3d4a04f9 100644
--- a/spec/support/shared_contexts/graphql/requests/packages_shared_context.rb
+++ b/spec/support/shared_contexts/graphql/requests/packages_shared_context.rb
@@ -10,6 +10,7 @@ RSpec.shared_context 'package details setup' do
let(:excluded) { %w[metadata apiFuzzingCiConfiguration pipeline packageFiles] }
let(:package_files) { all_graphql_fields_for('PackageFile') }
let(:dependency_links) { all_graphql_fields_for('PackageDependencyLink') }
+ let(:pipelines) { all_graphql_fields_for('Pipeline', max_depth: 1) }
let(:user) { project.owner }
let(:package_details) { graphql_data_at(:package) }
let(:metadata_response) { graphql_data_at(:package, :metadata) }
@@ -34,6 +35,11 @@ RSpec.shared_context 'package details setup' do
#{dependency_links}
}
}
+ pipelines {
+ nodes {
+ #{pipelines}
+ }
+ }
FIELDS
end
end
diff --git a/spec/support/shared_contexts/lib/gitlab/database/background_migration_job_shared_context.rb b/spec/support/shared_contexts/lib/gitlab/database/background_migration_job_shared_context.rb
deleted file mode 100644
index 382eb796f8e..00000000000
--- a/spec/support/shared_contexts/lib/gitlab/database/background_migration_job_shared_context.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_context 'background migration job class' do
- let!(:job_class_name) { 'TestJob' }
- let!(:job_class) { Class.new }
- let!(:job_perform_method) do
- ->(*arguments) do
- Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
- # Value is 'TestJob' defined by :job_class_name in the let! above.
- # Scoping prohibits us from directly referencing job_class_name.
- RSpec.current_example.example_group_instance.job_class_name,
- arguments
- )
- end
- end
-
- before do
- job_class.define_method(:perform, job_perform_method)
- expect(Gitlab::BackgroundMigration).to receive(:migration_class_for).with(job_class_name).at_least(:once) { job_class }
- end
-end
diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb
index 2abc52fce85..bcc6abdc308 100644
--- a/spec/support/shared_contexts/navbar_structure_context.rb
+++ b/spec/support/shared_contexts/navbar_structure_context.rb
@@ -119,7 +119,7 @@ RSpec.shared_context 'project navbar structure' do
_('Repository'),
_('CI/CD'),
_('Monitor'),
- (s_('UsageQuota|Usage Quotas') if Feature.enabled?(:project_storage_ui, default_enabled: :yaml))
+ s_('UsageQuota|Usage Quotas')
]
}
].compact
diff --git a/spec/support/shared_contexts/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
index d7e4864cb08..8a90f887381 100644
--- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
@@ -15,7 +15,7 @@ RSpec.shared_context 'ProjectPolicy context' do
let(:base_guest_permissions) do
%i[
- award_emoji create_issue create_incident create_merge_request_in create_note
+ award_emoji create_issue create_merge_request_in create_note
create_project read_issue_board read_issue read_issue_iid read_issue_link
read_label read_issue_board_list read_milestone read_note read_project
read_project_for_iids read_project_member read_release read_snippet
@@ -25,10 +25,11 @@ RSpec.shared_context 'ProjectPolicy context' do
let(:base_reporter_permissions) do
%i[
- admin_issue admin_issue_link admin_label admin_issue_board_list create_snippet
- daily_statistics download_code download_wiki_code fork_project metrics_dashboard
- read_build read_commit_status read_confidential_issues
- read_container_image read_deployment read_environment read_merge_request
+ admin_issue admin_issue_link admin_label admin_issue_board_list
+ create_snippet create_incident daily_statistics download_code
+ download_wiki_code fork_project metrics_dashboard read_build
+ read_commit_status read_confidential_issues read_container_image
+ read_deployment read_environment read_merge_request
read_metrics_dashboard_annotation read_pipeline read_prometheus
read_sentry_issue update_issue
]
diff --git a/spec/support/shared_contexts/requests/api/debian_repository_shared_context.rb b/spec/support/shared_contexts/requests/api/debian_repository_shared_context.rb
new file mode 100644
index 00000000000..95b8b7ed9f8
--- /dev/null
+++ b/spec/support/shared_contexts/requests/api/debian_repository_shared_context.rb
@@ -0,0 +1,120 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'Debian repository shared context' do |container_type, can_freeze|
+ include_context 'workhorse headers'
+
+ before do
+ stub_feature_flags(debian_packages: true, debian_group_packages: true)
+ end
+
+ let_it_be(:private_container, freeze: can_freeze) { create(container_type, :private) }
+ let_it_be(:public_container, freeze: can_freeze) { create(container_type, :public) }
+ let_it_be(:user, freeze: true) { create(:user) }
+ let_it_be(:personal_access_token, freeze: true) { create(:personal_access_token, user: user) }
+
+ let_it_be(:private_distribution, freeze: true) { create("debian_#{container_type}_distribution", :with_file, container: private_container, codename: 'existing-codename') }
+ let_it_be(:private_distribution_key, freeze: true) { create("debian_#{container_type}_distribution_key", distribution: private_distribution) }
+ let_it_be(:private_component, freeze: true) { create("debian_#{container_type}_component", distribution: private_distribution, name: 'existing-component') }
+ let_it_be(:private_architecture_all, freeze: true) { create("debian_#{container_type}_architecture", distribution: private_distribution, name: 'all') }
+ let_it_be(:private_architecture, freeze: true) { create("debian_#{container_type}_architecture", distribution: private_distribution, name: 'existing-arch') }
+ let_it_be(:private_component_file) { create("debian_#{container_type}_component_file", component: private_component, architecture: private_architecture) }
+
+ let_it_be(:public_distribution, freeze: true) { create("debian_#{container_type}_distribution", :with_file, container: public_container, codename: 'existing-codename') }
+ let_it_be(:public_distribution_key, freeze: true) { create("debian_#{container_type}_distribution_key", distribution: public_distribution) }
+ let_it_be(:public_component, freeze: true) { create("debian_#{container_type}_component", distribution: public_distribution, name: 'existing-component') }
+ let_it_be(:public_architecture_all, freeze: true) { create("debian_#{container_type}_architecture", distribution: public_distribution, name: 'all') }
+ let_it_be(:public_architecture, freeze: true) { create("debian_#{container_type}_architecture", distribution: public_distribution, name: 'existing-arch') }
+ let_it_be(:public_component_file) { create("debian_#{container_type}_component_file", component: public_component, architecture: public_architecture) }
+
+ if container_type == :group
+ let_it_be(:private_project) { create(:project, :private, group: private_container) }
+ let_it_be(:public_project) { create(:project, :public, group: public_container) }
+ let_it_be(:private_project_distribution) { create(:debian_project_distribution, container: private_project, codename: 'existing-codename') }
+ let_it_be(:public_project_distribution) { create(:debian_project_distribution, container: public_project, codename: 'existing-codename') }
+
+ let(:project) { { private: private_project, public: public_project }[visibility_level] }
+ else
+ let_it_be(:private_project) { private_container }
+ let_it_be(:public_project) { public_container }
+ let_it_be(:private_project_distribution) { private_distribution }
+ let_it_be(:public_project_distribution) { public_distribution }
+ end
+
+ let_it_be(:private_package) { create(:debian_package, project: private_project, published_in: private_project_distribution) }
+ let_it_be(:public_package) { create(:debian_package, project: public_project, published_in: public_project_distribution) }
+
+ let(:visibility_level) { :public }
+
+ let(:distribution) { { private: private_distribution, public: public_distribution }[visibility_level] }
+ let(:architecture) { { private: private_architecture, public: public_architecture }[visibility_level] }
+ let(:component) { { private: private_component, public: public_component }[visibility_level] }
+ let(:component_file) { { private: private_component_file, public: public_component_file }[visibility_level] }
+ let(:package) { { private: private_package, public: public_package }[visibility_level] }
+ let(:letter) { package.name[0..2] == 'lib' ? package.name[0..3] : package.name[0] }
+
+ let(:method) { :get }
+
+ let(:workhorse_params) do
+ if method == :put
+ file_upload = fixture_file_upload("spec/fixtures/packages/debian/#{file_name}")
+ { file: file_upload }
+ else
+ {}
+ end
+ end
+
+ let(:api_params) { workhorse_params }
+
+ let(:auth_headers) { {} }
+ let(:wh_headers) do
+ if method == :put
+ workhorse_headers
+ else
+ {}
+ end
+ end
+
+ let(:headers) { auth_headers.merge(wh_headers) }
+
+ let(:send_rewritten_field) { true }
+
+ subject do
+ if method == :put
+ workhorse_finalize(
+ api(url),
+ method: method,
+ file_key: :file,
+ params: api_params,
+ headers: headers,
+ send_rewritten_field: send_rewritten_field
+ )
+ else
+ send method, api(url), headers: headers, params: api_params
+ end
+ end
+end
+
+RSpec.shared_context 'Debian repository auth headers' do |user_type, auth_method = :private_token|
+ let(:token) { user_type == :invalid_token ? 'wrong' : personal_access_token.token }
+
+ let(:auth_headers) do
+ if user_type == :anonymous
+ {}
+ elsif auth_method == :private_token
+ { 'Private-Token' => token }
+ else
+ basic_auth_header(user.username, token)
+ end
+ end
+end
+
+RSpec.shared_context 'Debian repository access' do |visibility_level, user_type, auth_method|
+ include_context 'Debian repository auth headers', user_type, auth_method do
+ let(:containers) { { private: private_container, public: public_container } }
+ let(:container) { containers[visibility_level] }
+
+ before do
+ container.send("add_#{user_type}", user) if user_type != :anonymous && user_type != :not_a_member && user_type != :invalid_token
+ end
+ end
+end
diff --git a/spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb b/spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb
index 80f011f622b..21be989d697 100644
--- a/spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb
+++ b/spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb
@@ -31,14 +31,14 @@ RSpec.shared_context 'container repository delete tags service shared context' d
end
end
- def stub_put_manifest_request(tag, status = 200, headers = { 'docker-content-digest' => 'sha256:dummy' })
+ def stub_put_manifest_request(tag, status = 200, headers = { DependencyProxy::Manifest::DIGEST_HEADER => 'sha256:dummy' })
stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/#{tag}")
.to_return(status: status, body: '', headers: headers)
end
def stub_tag_digest(tag, digest)
stub_request(:head, "http://registry.gitlab/v2/#{repository.path}/manifests/#{tag}")
- .to_return(status: 200, body: '', headers: { 'docker-content-digest' => digest })
+ .to_return(status: 200, body: '', headers: { DependencyProxy::Manifest::DIGEST_HEADER => digest })
end
def stub_digest_config(digest, created_at)
diff --git a/spec/support/shared_contexts/services/service_ping/stubbed_service_ping_metrics_definitions_shared_context.rb b/spec/support/shared_contexts/services/service_ping/stubbed_service_ping_metrics_definitions_shared_context.rb
index 2b810e790f0..e1d864213b5 100644
--- a/spec/support/shared_contexts/services/service_ping/stubbed_service_ping_metrics_definitions_shared_context.rb
+++ b/spec/support/shared_contexts/services/service_ping/stubbed_service_ping_metrics_definitions_shared_context.rb
@@ -38,6 +38,11 @@ RSpec.shared_context 'stubbed service ping metrics definitions' do
)
end
+ after do |example|
+ Gitlab::Usage::Metric.instance_variable_set(:@all, nil)
+ Gitlab::Usage::MetricDefinition.instance_variable_set(:@all, nil)
+ end
+
def metric_attributes(key_path, category, value_type = 'string')
{
'key_path' => key_path,
diff --git a/spec/support/shared_contexts/url_shared_context.rb b/spec/support/shared_contexts/url_shared_context.rb
index f3d227b6e2b..da1d6e0049c 100644
--- a/spec/support/shared_contexts/url_shared_context.rb
+++ b/spec/support/shared_contexts/url_shared_context.rb
@@ -1,19 +1,32 @@
# frozen_string_literal: true
+RSpec.shared_context 'valid urls with CRLF' do
+ let(:valid_urls_with_CRLF) do
+ [
+ "http://example.com/pa%0dth",
+ "http://example.com/pa%0ath",
+ "http://example.com/pa%0d%0th",
+ "http://example.com/pa%0D%0Ath",
+ "http://gitlab.com/path?param=foo%0Abar",
+ "https://gitlab.com/path?param=foo%0Dbar",
+ "http://example.org:1024/path?param=foo%0D%0Abar",
+ "https://storage.googleapis.com/bucket/import_export_upload/import_file/57265/express.tar.gz?GoogleAccessId=hello@example.org&Signature=ABCD%0AEFGHik&Expires=1634663304"
+ ]
+ end
+end
+
RSpec.shared_context 'invalid urls' do
let(:urls_with_CRLF) do
- ["http://127.0.0.1:333/pa\rth",
- "http://127.0.0.1:333/pa\nth",
- "http://127.0a.0.1:333/pa\r\nth",
- "http://127.0.0.1:333/path?param=foo\r\nbar",
- "http://127.0.0.1:333/path?param=foo\rbar",
- "http://127.0.0.1:333/path?param=foo\nbar",
- "http://127.0.0.1:333/pa%0dth",
- "http://127.0.0.1:333/pa%0ath",
- "http://127.0a.0.1:333/pa%0d%0th",
- "http://127.0.0.1:333/pa%0D%0Ath",
- "http://127.0.0.1:333/path?param=foo%0Abar",
- "http://127.0.0.1:333/path?param=foo%0Dbar",
- "http://127.0.0.1:333/path?param=foo%0D%0Abar"]
+ [
+ "git://example.com/pa%0dth",
+ "git://example.com/pa%0ath",
+ "git://example.com/pa%0d%0th",
+ "http://example.com/pa\rth",
+ "http://example.com/pa\nth",
+ "http://example.com/pa\r\nth",
+ "http://example.com/path?param=foo\r\nbar",
+ "http://example.com/path?param=foo\rbar",
+ "http://example.com/path?param=foo\nbar"
+ ]
end
end
diff --git a/spec/support/shared_examples/bulk_imports/common/pipelines/wiki_pipeline_examples.rb b/spec/support/shared_examples/bulk_imports/common/pipelines/wiki_pipeline_examples.rb
new file mode 100644
index 00000000000..e8cc666605b
--- /dev/null
+++ b/spec/support/shared_examples/bulk_imports/common/pipelines/wiki_pipeline_examples.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'wiki pipeline imports a wiki for an entity' do
+ describe '#run' do
+ let_it_be(:bulk_import_configuration) { create(:bulk_import_configuration, bulk_import: bulk_import) }
+
+ let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) }
+ let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
+
+ let(:extracted_data) { BulkImports::Pipeline::ExtractedData.new(data: {}) }
+
+ context 'successfully imports wiki for an entity' do
+ subject { described_class.new(context) }
+
+ before do
+ allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor|
+ allow(extractor).to receive(:extract).and_return(extracted_data)
+ end
+ end
+
+ it 'imports new wiki into destination project' do
+ expect_next_instance_of(Gitlab::GitalyClient::RepositoryService) do |repository_service|
+ url = "https://oauth2:token@gitlab.example/#{entity.source_full_path}.wiki.git"
+ expect(repository_service).to receive(:fetch_remote).with(url, any_args).and_return 0
+ end
+
+ subject.run
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/controllers/concerns/integrations_actions_shared_examples.rb b/spec/support/shared_examples/controllers/concerns/integrations/integrations_actions_shared_examples.rb
index 748a3acf17b..a8aed0c1f0b 100644
--- a/spec/support/shared_examples/controllers/concerns/integrations_actions_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/concerns/integrations/integrations_actions_shared_examples.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-RSpec.shared_examples IntegrationsActions do
+RSpec.shared_examples Integrations::Actions do
let(:integration) do
create(:datadog_integration,
integration_attributes.merge(
diff --git a/spec/support/shared_examples/controllers/create_notes_rate_limit_shared_examples.rb b/spec/support/shared_examples/controllers/create_notes_rate_limit_shared_examples.rb
index 74a98c20383..8affe4ac8f5 100644
--- a/spec/support/shared_examples/controllers/create_notes_rate_limit_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/create_notes_rate_limit_shared_examples.rb
@@ -6,39 +6,41 @@
# - request_full_path
RSpec.shared_examples 'request exceeding rate limit' do
- before do
- stub_application_setting(notes_create_limit: 2)
- 2.times { post :create, params: params }
- end
+ context 'with rate limiter', :freeze_time, :clean_gitlab_redis_rate_limiting do
+ before do
+ stub_application_setting(notes_create_limit: 2)
+ 2.times { post :create, params: params }
+ end
- it 'prevents from creating more notes', :request_store do
- expect { post :create, params: params }
- .to change { Note.count }.by(0)
+ it 'prevents from creating more notes' do
+ expect { post :create, params: params }
+ .to change { Note.count }.by(0)
- expect(response).to have_gitlab_http_status(:too_many_requests)
- expect(response.body).to eq(_('This endpoint has been requested too many times. Try again later.'))
- end
+ expect(response).to have_gitlab_http_status(:too_many_requests)
+ expect(response.body).to eq(_('This endpoint has been requested too many times. Try again later.'))
+ 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: request_full_path,
- user_id: user.id,
- username: user.username
- }
+ 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: request_full_path,
+ user_id: user.id,
+ username: user.username
+ }
- expect(Gitlab::AuthLogger).to receive(:error).with(attributes).once
- post :create, params: params
- end
+ expect(Gitlab::AuthLogger).to receive(:error).with(attributes).once
+ post :create, params: params
+ 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}"])
+ 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}"])
- post :create, params: params
- expect(response).to have_gitlab_http_status(:found)
+ post :create, params: params
+ expect(response).to have_gitlab_http_status(:found)
+ end
end
end
diff --git a/spec/support/shared_examples/features/2fa_shared_examples.rb b/spec/support/shared_examples/features/2fa_shared_examples.rb
index ddc03e178ba..94c91556ea7 100644
--- a/spec/support/shared_examples/features/2fa_shared_examples.rb
+++ b/spec/support/shared_examples/features/2fa_shared_examples.rb
@@ -18,6 +18,7 @@ RSpec.shared_examples 'hardware device for 2fa' do |device_type|
let(:user) { create(:user) }
before do
+ stub_feature_flags(bootstrap_confirmation_modals: false)
gitlab_sign_in(user)
user.update_attribute(:otp_required_for_login, true)
end
diff --git a/spec/support/shared_examples/features/dependency_proxy_shared_examples.rb b/spec/support/shared_examples/features/dependency_proxy_shared_examples.rb
index d29c677a962..5d1488502d2 100644
--- a/spec/support/shared_examples/features/dependency_proxy_shared_examples.rb
+++ b/spec/support/shared_examples/features/dependency_proxy_shared_examples.rb
@@ -26,7 +26,7 @@ RSpec.shared_examples 'a successful manifest pull' do
subject
expect(response).to have_gitlab_http_status(:ok)
- expect(response.headers['Docker-Content-Digest']).to eq(manifest.digest)
+ expect(response.headers[DependencyProxy::Manifest::DIGEST_HEADER]).to eq(manifest.digest)
expect(response.headers['Content-Length']).to eq(manifest.size)
expect(response.headers['Docker-Distribution-Api-Version']).to eq(DependencyProxy::DISTRIBUTION_API_VERSION)
expect(response.headers['Etag']).to eq("\"#{manifest.digest}\"")
diff --git a/spec/support/shared_examples/features/manage_applications_shared_examples.rb b/spec/support/shared_examples/features/manage_applications_shared_examples.rb
index 0161899cb76..27d50c67f24 100644
--- a/spec/support/shared_examples/features/manage_applications_shared_examples.rb
+++ b/spec/support/shared_examples/features/manage_applications_shared_examples.rb
@@ -18,6 +18,7 @@ RSpec.shared_examples 'manage applications' do
click_on 'Save application'
validate_application(application_name, 'Yes')
+ expect(page).to have_link('Continue', href: index_path)
application = Doorkeeper::Application.find_by(name: application_name)
expect(page).to have_css("button[title=\"Copy secret\"][data-clipboard-text=\"#{application.secret}\"]", text: 'Copy')
@@ -33,6 +34,7 @@ RSpec.shared_examples 'manage applications' do
click_on 'Save application'
validate_application(application_name_changed, 'No')
+ expect(page).not_to have_link('Continue')
visit_applications_path
diff --git a/spec/support/shared_examples/features/packages_shared_examples.rb b/spec/support/shared_examples/features/packages_shared_examples.rb
index 96be30b9f1f..d14b4638ca5 100644
--- a/spec/support/shared_examples/features/packages_shared_examples.rb
+++ b/spec/support/shared_examples/features/packages_shared_examples.rb
@@ -21,10 +21,6 @@ end
RSpec.shared_examples 'package details link' do |property|
let(:package) { packages.first }
- before do
- stub_feature_flags(packages_details_one_column: false)
- end
-
it 'navigates to the correct url' do
page.within(packages_table_selector) do
click_link package.name
@@ -32,7 +28,7 @@ RSpec.shared_examples 'package details link' do |property|
expect(page).to have_current_path(project_package_path(package.project, package))
- expect(page).to have_css('.packages-app h1[data-testid="title"]', text: package.name)
+ expect(page).to have_css('.packages-app h2[data-testid="title"]', text: package.name)
expect(page).to have_content('Installation')
expect(page).to have_content('Registry setup')
@@ -94,16 +90,24 @@ def packages_table_selector
end
def click_sort_option(option, ascending)
- page.within('.gl-sorting') do
- # Reset the sort direction
- click_button 'Sort direction' if page.has_selector?('svg[aria-label="Sorting Direction: Ascending"]', wait: 0)
+ wait_for_requests
- find('button.gl-dropdown-toggle').click
+ # Reset the sort direction
+ if page.has_selector?('button[aria-label="Sorting Direction: Ascending"]', wait: 0) && !ascending
+ click_button 'Sort direction'
- page.within('.dropdown-menu') do
- click_button option
- end
+ wait_for_requests
+ end
+
+ find('button.gl-dropdown-toggle').click
+
+ page.within('.dropdown-menu') do
+ click_button option
+ end
+
+ if ascending
+ wait_for_requests
- click_button 'Sort direction' if ascending
+ click_button 'Sort direction'
end
end
diff --git a/spec/support/shared_examples/features/resolving_discussions_in_issues_shared_examples.rb b/spec/support/shared_examples/features/resolving_discussions_in_issues_shared_examples.rb
index 6d44a6fde85..337b3f3cbd0 100644
--- a/spec/support/shared_examples/features/resolving_discussions_in_issues_shared_examples.rb
+++ b/spec/support/shared_examples/features/resolving_discussions_in_issues_shared_examples.rb
@@ -1,43 +1,29 @@
# frozen_string_literal: true
RSpec.shared_examples 'creating an issue for a thread' do
- it 'shows an issue with the title filled in' do
+ it 'shows an issue creation form' do
+ # Title field is filled in
title_field = page.find_field('issue[title]')
-
expect(title_field.value).to include(merge_request.title)
- end
- it 'has a mention of the discussion in the description' do
- description_field = page.find_field('issue[description]')
+ # Has a hidden field for the merge request
+ merge_request_field = find('#merge_request_to_resolve_discussions_of', visible: false)
+ expect(merge_request_field.value).to eq(merge_request.iid.to_s)
+ # Has a mention of the discussion in the description
+ description_field = page.find_field('issue[description]')
expect(description_field.value).to include(discussion.first_note.note)
end
- it 'can create a new issue for the project' do
+ it 'creates a new issue for the project' do
+ # Actually creates an issue for the project
expect { click_button 'Create issue' }.to change { project.issues.reload.size }.by(1)
- end
-
- it 'resolves the discussion in the merge request' do
- click_button 'Create issue'
+ # Resolves the discussion in the merge request
discussion.first_note.reload
-
expect(discussion.resolved?).to eq(true)
- end
-
- it 'shows a flash messaage after resolving a discussion' do
- click_button 'Create issue'
-
- page.within '.flash-notice' do
- # Only check for the word 'Resolved' since the spec might have resolved
- # multiple discussions
- expect(page).to have_content('Resolved')
- end
- end
-
- it 'has a hidden field for the merge request' do
- merge_request_field = find('#merge_request_to_resolve_discussions_of', visible: false)
- expect(merge_request_field.value).to eq(merge_request.iid.to_s)
+ # Issue title inludes MR title
+ expect(page).to have_content(%Q(Follow-up from "#{merge_request.title}"))
end
end
diff --git a/spec/support/shared_examples/features/sidebar_shared_examples.rb b/spec/support/shared_examples/features/sidebar_shared_examples.rb
index 5bfe929e957..d509d124de0 100644
--- a/spec/support/shared_examples/features/sidebar_shared_examples.rb
+++ b/spec/support/shared_examples/features/sidebar_shared_examples.rb
@@ -52,16 +52,17 @@ RSpec.shared_examples 'issue boards sidebar' do
it 'shows toggle as on then as off as user toggles to subscribe and unsubscribe', :aggregate_failures do
wait_for_requests
+ subscription_button = find('[data-testid="subscription-toggle"]')
- click_button 'Notifications'
+ subscription_button.click
- expect(page).to have_button('Notifications', class: 'is-checked')
+ expect(subscription_button).to have_css("button.is-checked")
- click_button 'Notifications'
+ subscription_button.click
wait_for_requests
- expect(page).not_to have_button('Notifications', class: 'is-checked')
+ expect(subscription_button).to have_css("button:not(.is-checked)")
end
context 'when notifications have been disabled' do
@@ -73,7 +74,7 @@ RSpec.shared_examples 'issue boards sidebar' do
it 'displays a message that notifications have been disabled' do
page.within('[data-testid="sidebar-notifications"]') do
- expect(page).to have_button('Notifications', class: 'is-disabled')
+ expect(page).to have_selector('[data-testid="subscription-toggle"]', class: 'is-disabled')
expect(page).to have_content('Disabled by project owner')
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 fb598b978f6..56b6dc682eb 100644
--- a/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb
@@ -66,20 +66,22 @@ RSpec.shared_examples 'a Note mutation when the given resource id is not for a N
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
+ context 'with rate limiter', :freeze_time, :clean_gitlab_redis_rate_limiting do
before do
- stub_application_setting(notes_create_limit_allowlist: ["#{current_user.username}"])
+ 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 creates a Note'
+ 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
end
diff --git a/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb
index 8b4ecd7d5ae..a3c67210a4a 100644
--- a/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb
@@ -35,8 +35,8 @@ RSpec.shared_examples 'common trace features' do
stub_feature_flags(gitlab_ci_archived_trace_consistent_reads: trace.job.project)
end
- it 'calls ::ApplicationRecord.sticking.unstick_or_continue_sticking' do
- expect(::ApplicationRecord.sticking).to receive(:unstick_or_continue_sticking)
+ it 'calls ::Ci::Build.sticking.unstick_or_continue_sticking' do
+ expect(::Ci::Build.sticking).to receive(:unstick_or_continue_sticking)
.with(described_class::LOAD_BALANCING_STICKING_NAMESPACE, trace.job.id)
.and_call_original
@@ -49,8 +49,8 @@ RSpec.shared_examples 'common trace features' do
stub_feature_flags(gitlab_ci_archived_trace_consistent_reads: false)
end
- it 'does not call ::ApplicationRecord.sticking.unstick_or_continue_sticking' do
- expect(::ApplicationRecord.sticking).not_to receive(:unstick_or_continue_sticking)
+ it 'does not call ::Ci::Build.sticking.unstick_or_continue_sticking' do
+ expect(::Ci::Build.sticking).not_to receive(:unstick_or_continue_sticking)
trace.read { |stream| stream }
end
@@ -305,8 +305,8 @@ RSpec.shared_examples 'common trace features' do
stub_feature_flags(gitlab_ci_archived_trace_consistent_reads: trace.job.project)
end
- it 'calls ::ApplicationRecord.sticking.stick' do
- expect(::ApplicationRecord.sticking).to receive(:stick)
+ it 'calls ::Ci::Build.sticking.stick' do
+ expect(::Ci::Build.sticking).to receive(:stick)
.with(described_class::LOAD_BALANCING_STICKING_NAMESPACE, trace.job.id)
.and_call_original
@@ -319,8 +319,8 @@ RSpec.shared_examples 'common trace features' do
stub_feature_flags(gitlab_ci_archived_trace_consistent_reads: false)
end
- it 'does not call ::ApplicationRecord.sticking.stick' do
- expect(::ApplicationRecord.sticking).not_to receive(:stick)
+ it 'does not call ::Ci::Build.sticking.stick' do
+ expect(::Ci::Build.sticking).not_to receive(:stick)
subject
end
@@ -808,7 +808,19 @@ RSpec.shared_examples 'trace with enabled live trace feature' do
create(:ci_job_artifact, :trace, job: build)
end
- it { is_expected.to be_truthy }
+ it 'is truthy' do
+ is_expected.to be_truthy
+ end
+ end
+
+ context 'when archived trace record exists but file is not stored' do
+ before do
+ create(:ci_job_artifact, :unarchived_trace_artifact, job: build)
+ end
+
+ it 'is falsy' do
+ is_expected.to be_falsy
+ end
end
context 'when live trace exists' do
@@ -872,13 +884,35 @@ RSpec.shared_examples 'trace with enabled live trace feature' do
build.reload
expect(build.trace.exist?).to be_truthy
- expect(build.job_artifacts_trace).to be_nil
Gitlab::Ci::Trace::ChunkedIO.new(build) do |stream|
expect(stream.read).to eq(trace_raw)
end
end
end
+ shared_examples 'a pre-commit error' do |error:|
+ it_behaves_like 'source trace in ChunkedIO stays intact', error: error
+
+ it 'does not save the trace artifact' do
+ expect { subject }.to raise_error(error)
+
+ build.reload
+ expect(build.job_artifacts_trace).to be_nil
+ end
+ end
+
+ shared_examples 'a post-commit error' do |error:|
+ it_behaves_like 'source trace in ChunkedIO stays intact', error: error
+
+ it 'saves the trace artifact but not the file' do
+ expect { subject }.to raise_error(error)
+
+ build.reload
+ expect(build.job_artifacts_trace).to be_present
+ expect(build.job_artifacts_trace.file.exists?).to be_falsy
+ end
+ end
+
context 'when job does not have trace artifact' do
context 'when trace is stored in ChunkedIO' do
let!(:build) { create(:ci_build, :success, :trace_live) }
@@ -892,7 +926,7 @@ RSpec.shared_examples 'trace with enabled live trace feature' do
allow(IO).to receive(:copy_stream).and_return(0)
end
- it_behaves_like 'source trace in ChunkedIO stays intact', error: Gitlab::Ci::Trace::ArchiveError
+ it_behaves_like 'a pre-commit error', error: Gitlab::Ci::Trace::ArchiveError
end
context 'when failed to create job artifact record' do
@@ -902,7 +936,16 @@ RSpec.shared_examples 'trace with enabled live trace feature' do
.and_return(%w[Error Error])
end
- it_behaves_like 'source trace in ChunkedIO stays intact', error: ActiveRecord::RecordInvalid
+ it_behaves_like 'a pre-commit error', error: ActiveRecord::RecordInvalid
+ end
+
+ context 'when storing the file raises an error' do
+ before do
+ stub_artifacts_object_storage(direct_upload: true)
+ allow_any_instance_of(Ci::JobArtifact).to receive(:store_file!).and_raise(Excon::Error::BadGateway, 'S3 is down lol')
+ end
+
+ it_behaves_like 'a post-commit error', error: Excon::Error::BadGateway
end
end
end
diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb
index 6342064beb8..bea7cca2744 100644
--- a/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb
+++ b/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb
@@ -6,7 +6,7 @@ shared_examples 'deployment metrics examples' do
environment = project.environments.production.first || create(:environment, :production, project: project)
create(:deployment, :success, args.merge(environment: environment))
- # this is needed for the dora_deployment_frequency_in_vsa feature flag so we have aggregated data
+ # this is needed for the DORA API so we have aggregated data
::Dora::DailyMetrics::RefreshWorker.new.perform(environment.id, Time.current.to_date.to_s) if Gitlab.ee?
end
diff --git a/spec/support/shared_examples/lib/gitlab/database/cte_materialized_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/cte_materialized_shared_examples.rb
index a617342ff8c..df795723874 100644
--- a/spec/support/shared_examples/lib/gitlab/database/cte_materialized_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/database/cte_materialized_shared_examples.rb
@@ -11,7 +11,7 @@ RSpec.shared_examples 'CTE with MATERIALIZED keyword examples' do
context 'when PG version is <12' do
it 'does not add MATERIALIZE keyword' do
- allow(Gitlab::Database.main).to receive(:version).and_return('11.1')
+ allow(ApplicationRecord.database).to receive(:version).and_return('11.1')
expect(query).to include(expected_query_block_without_materialized)
end
@@ -19,14 +19,14 @@ RSpec.shared_examples 'CTE with MATERIALIZED keyword examples' do
context 'when PG version is >=12' do
it 'adds MATERIALIZE keyword' do
- allow(Gitlab::Database.main).to receive(:version).and_return('12.1')
+ allow(ApplicationRecord.database).to receive(:version).and_return('12.1')
expect(query).to include(expected_query_block_with_materialized)
end
context 'when version is higher than 12' do
it 'adds MATERIALIZE keyword' do
- allow(Gitlab::Database.main).to receive(:version).and_return('15.1')
+ allow(ApplicationRecord.database).to receive(:version).and_return('15.1')
expect(query).to include(expected_query_block_with_materialized)
end
diff --git a/spec/support/shared_examples/lib/gitlab/import_export/attributes_permitter_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/import_export/attributes_permitter_shared_examples.rb
index 5ce698c4701..41d3d76b66b 100644
--- a/spec/support/shared_examples/lib/gitlab/import_export/attributes_permitter_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/import_export/attributes_permitter_shared_examples.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
-RSpec.shared_examples 'a permitted attribute' do |relation_sym, permitted_attributes|
+RSpec.shared_examples 'a permitted attribute' do |relation_sym, permitted_attributes, additional_attributes = []|
let(:prohibited_attributes) { %i[remote_url my_attributes my_ids token my_id test] }
let(:import_export_config) { Gitlab::ImportExport::Config.new.to_h }
@@ -26,7 +26,7 @@ RSpec.shared_examples 'a permitted attribute' do |relation_sym, permitted_attrib
end
it 'does not contain attributes that would be cleaned with AttributeCleaner' do
- expect(cleaned_hash.keys).to include(*permitted_hash.keys)
+ expect(cleaned_hash.keys + additional_attributes.to_a).to include(*permitted_hash.keys)
end
it 'does not contain prohibited attributes that are not related to given relation' do
diff --git a/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb
index 708bc71ae96..ff03051ed37 100644
--- a/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb
@@ -2,7 +2,7 @@
RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name|
let(:fake_duplicate_job) do
- instance_double(Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob)
+ instance_double(Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, duplicate_key_ttl: Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob::DEFAULT_DUPLICATE_KEY_TTL)
end
let(:expected_message) { "dropped #{strategy_name.to_s.humanize.downcase}" }
@@ -11,14 +11,14 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name|
describe '#schedule' do
before do
- allow(Gitlab::SidekiqLogging::DeduplicationLogger.instance).to receive(:log)
+ allow(Gitlab::SidekiqLogging::DeduplicationLogger.instance).to receive(:deduplicated_log)
end
it 'checks for duplicates before yielding' do
expect(fake_duplicate_job).to receive(:scheduled?).twice.ordered.and_return(false)
expect(fake_duplicate_job).to(
receive(:check!)
- .with(Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob::DUPLICATE_KEY_TTL)
+ .with(fake_duplicate_job.duplicate_key_ttl)
.ordered
.and_return('a jid'))
expect(fake_duplicate_job).to receive(:duplicate?).ordered.and_return(false)
@@ -40,6 +40,7 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name|
allow(fake_duplicate_job).to receive(:check!).and_return('the jid')
allow(fake_duplicate_job).to receive(:idempotent?).and_return(true)
allow(fake_duplicate_job).to receive(:update_latest_wal_location!)
+ allow(fake_duplicate_job).to receive(:set_deduplicated_flag!)
allow(fake_duplicate_job).to receive(:options).and_return({})
job_hash = {}
@@ -61,10 +62,11 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name|
allow(fake_duplicate_job).to receive(:options).and_return({ including_scheduled: true })
allow(fake_duplicate_job).to(
receive(:check!)
- .with(Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob::DUPLICATE_KEY_TTL)
+ .with(fake_duplicate_job.duplicate_key_ttl)
.and_return('the jid'))
allow(fake_duplicate_job).to receive(:idempotent?).and_return(true)
allow(fake_duplicate_job).to receive(:update_latest_wal_location!)
+ allow(fake_duplicate_job).to receive(:set_deduplicated_flag!)
job_hash = {}
expect(fake_duplicate_job).to receive(:duplicate?).and_return(true)
@@ -83,9 +85,10 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name|
allow(fake_duplicate_job).to receive(:scheduled_at).and_return(Time.now + time_diff)
allow(fake_duplicate_job).to receive(:options).and_return({ including_scheduled: true })
allow(fake_duplicate_job).to(
- receive(:check!).with(time_diff.to_i).and_return('the jid'))
+ receive(:check!).with(time_diff.to_i + fake_duplicate_job.duplicate_key_ttl).and_return('the jid'))
allow(fake_duplicate_job).to receive(:idempotent?).and_return(true)
allow(fake_duplicate_job).to receive(:update_latest_wal_location!)
+ allow(fake_duplicate_job).to receive(:set_deduplicated_flag!)
job_hash = {}
expect(fake_duplicate_job).to receive(:duplicate?).and_return(true)
@@ -100,6 +103,26 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name|
end
end
+ context "when the job is not duplicate" do
+ before do
+ allow(fake_duplicate_job).to receive(:scheduled?).and_return(false)
+ allow(fake_duplicate_job).to receive(:check!).and_return('the jid')
+ allow(fake_duplicate_job).to receive(:duplicate?).and_return(false)
+ allow(fake_duplicate_job).to receive(:options).and_return({})
+ allow(fake_duplicate_job).to receive(:existing_jid).and_return('the jid')
+ end
+
+ it 'does not return false nor drop the job' do
+ schedule_result = nil
+
+ expect(fake_duplicate_job).not_to receive(:set_deduplicated_flag!)
+
+ expect { |b| schedule_result = strategy.schedule({}, &b) }.to yield_control
+
+ expect(schedule_result).to be_nil
+ end
+ end
+
context "when the job is droppable" do
before do
allow(fake_duplicate_job).to receive(:scheduled?).and_return(false)
@@ -109,6 +132,7 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name|
allow(fake_duplicate_job).to receive(:existing_jid).and_return('the jid')
allow(fake_duplicate_job).to receive(:idempotent?).and_return(true)
allow(fake_duplicate_job).to receive(:update_latest_wal_location!)
+ allow(fake_duplicate_job).to receive(:set_deduplicated_flag!)
end
it 'updates latest wal location' do
@@ -117,10 +141,11 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name|
strategy.schedule({ 'jid' => 'new jid' }) {}
end
- it 'drops the job' do
+ it 'returns false to drop the job' do
schedule_result = nil
expect(fake_duplicate_job).to receive(:idempotent?).and_return(true)
+ expect(fake_duplicate_job).to receive(:set_deduplicated_flag!).once
expect { |b| schedule_result = strategy.schedule({}, &b) }.not_to yield_control
expect(schedule_result).to be(false)
@@ -130,7 +155,7 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name|
fake_logger = instance_double(Gitlab::SidekiqLogging::DeduplicationLogger)
expect(Gitlab::SidekiqLogging::DeduplicationLogger).to receive(:instance).and_return(fake_logger)
- expect(fake_logger).to receive(:log).with(a_hash_including({ 'jid' => 'new jid' }), expected_message, {})
+ expect(fake_logger).to receive(:deduplicated_log).with(a_hash_including({ 'jid' => 'new jid' }), expected_message, {})
strategy.schedule({ 'jid' => 'new jid' }) {}
end
@@ -140,7 +165,7 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name|
expect(Gitlab::SidekiqLogging::DeduplicationLogger).to receive(:instance).and_return(fake_logger)
allow(fake_duplicate_job).to receive(:options).and_return({ foo: :bar })
- expect(fake_logger).to receive(:log).with(a_hash_including({ 'jid' => 'new jid' }), expected_message, { foo: :bar })
+ expect(fake_logger).to receive(:deduplicated_log).with(a_hash_including({ 'jid' => 'new jid' }), expected_message, { foo: :bar })
strategy.schedule({ 'jid' => 'new jid' }) {}
end
@@ -159,6 +184,9 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name|
before do
allow(fake_duplicate_job).to receive(:delete!)
+ allow(fake_duplicate_job).to receive(:scheduled?) { false }
+ allow(fake_duplicate_job).to receive(:options) { {} }
+ allow(fake_duplicate_job).to receive(:should_reschedule?) { false }
allow(fake_duplicate_job).to receive(:latest_wal_locations).and_return( wal_locations )
end
diff --git a/spec/support/shared_examples/lib/sidebars/projects/menus/zentao_menu_shared_examples.rb b/spec/support/shared_examples/lib/sidebars/projects/menus/zentao_menu_shared_examples.rb
new file mode 100644
index 00000000000..d3fd28727b5
--- /dev/null
+++ b/spec/support/shared_examples/lib/sidebars/projects/menus/zentao_menu_shared_examples.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples 'ZenTao menu with CE version' do
+ let(:project) { create(:project, has_external_issue_tracker: true) }
+ let(:user) { project.owner }
+ let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
+ let(:zentao_integration) { create(:zentao_integration, project: project) }
+
+ subject { described_class.new(context) }
+
+ describe '#render?' do
+ context 'when issues integration is disabled' do
+ before do
+ zentao_integration.update!(active: false)
+ end
+
+ it 'returns false' do
+ expect(subject.render?).to eq false
+ end
+ end
+
+ context 'when issues integration is enabled' do
+ before do
+ zentao_integration.update!(active: true)
+ end
+
+ it 'returns true' do
+ expect(subject.render?).to eq true
+ end
+
+ it 'renders menu link' do
+ expect(subject.link).to eq zentao_integration.url
+ end
+
+ it 'contains only open ZenTao item' do
+ expect(subject.renderable_items.map(&:item_id)).to match_array [:open_zentao]
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb b/spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb
new file mode 100644
index 00000000000..7ccd9533811
--- /dev/null
+++ b/spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'it has loose foreign keys' do
+ let(:factory_name) { nil }
+ let(:table_name) { described_class.table_name }
+ let(:connection) { described_class.connection }
+
+ it 'includes the LooseForeignKey module' do
+ expect(described_class.ancestors).to include(LooseForeignKey)
+ end
+
+ it 'responds to #loose_foreign_key_definitions' do
+ expect(described_class).to respond_to(:loose_foreign_key_definitions)
+ end
+
+ it 'has at least one loose foreign key definition' do
+ expect(described_class.loose_foreign_key_definitions.size).to be > 0
+ end
+
+ it 'has the deletion trigger present' do
+ sql = <<-SQL
+ SELECT trigger_name
+ FROM information_schema.triggers
+ WHERE event_object_table = '#{table_name}'
+ SQL
+
+ triggers = connection.execute(sql)
+
+ expected_trigger_name = "#{table_name}_loose_fk_trigger"
+ expect(triggers.pluck('trigger_name')).to include(expected_trigger_name)
+ end
+
+ it 'records record deletions' do
+ model = create(factory_name) # rubocop: disable Rails/SaveBang
+ model.destroy!
+
+ deleted_record = LooseForeignKeys::DeletedRecord.find_by(fully_qualified_table_name: "#{connection.current_schema}.#{table_name}", primary_key_value: model.id)
+
+ expect(deleted_record).not_to be_nil
+ end
+
+ it 'cleans up record deletions' do
+ model = create(factory_name) # rubocop: disable Rails/SaveBang
+
+ expect { model.destroy! }.to change { LooseForeignKeys::DeletedRecord.count }.by(1)
+
+ LooseForeignKeys::ProcessDeletedRecordsService.new(connection: connection).execute
+
+ expect(LooseForeignKeys::DeletedRecord.status_pending.count).to be(0)
+ expect(LooseForeignKeys::DeletedRecord.status_processed.count).to be(1)
+ end
+end
diff --git a/spec/support/shared_examples/metrics/transaction_metrics_with_labels_shared_examples.rb b/spec/support/shared_examples/metrics/transaction_metrics_with_labels_shared_examples.rb
new file mode 100644
index 00000000000..286c60f1f4f
--- /dev/null
+++ b/spec/support/shared_examples/metrics/transaction_metrics_with_labels_shared_examples.rb
@@ -0,0 +1,219 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'transaction metrics with labels' do
+ let(:sensitive_tags) do
+ {
+ path: 'private',
+ branch: 'sensitive'
+ }
+ end
+
+ around do |example|
+ described_class.reload_metric!
+ example.run
+ described_class.reload_metric!
+ end
+
+ describe '.prometheus_metric' do
+ let(:prometheus_metric) { instance_double(Prometheus::Client::Histogram, observe: nil, base_labels: {}) }
+
+ it 'adds a metric' do
+ expect(::Gitlab::Metrics).to receive(:histogram).with(
+ :meow_observe, 'Meow observe histogram', hash_including(*described_class::BASE_LABEL_KEYS), be_a(Array)
+ ).and_return(prometheus_metric)
+
+ expect do |block|
+ metric = described_class.prometheus_metric(:meow_observe, :histogram, &block)
+ expect(metric).to be(prometheus_metric)
+ end.to yield_control
+ end
+ end
+
+ describe '#method_call_for' do
+ it 'returns a MethodCall' do
+ method = transaction_obj.method_call_for('Foo#bar', :Foo, '#bar')
+
+ expect(method).to be_an_instance_of(Gitlab::Metrics::MethodCall)
+ end
+ end
+
+ describe '#add_event' do
+ let(:prometheus_metric) { instance_double(Prometheus::Client::Counter, increment: nil, base_labels: {}) }
+
+ it 'adds a metric' do
+ expect(prometheus_metric).to receive(:increment).with(labels)
+ expect(described_class).to receive(:fetch_metric).with(:counter, :gitlab_transaction_event_meow_total).and_return(prometheus_metric)
+
+ transaction_obj.add_event(:meow)
+ end
+
+ it 'allows tracking of custom tags' do
+ expect(prometheus_metric).to receive(:increment).with(labels.merge(animal: "dog"))
+ expect(described_class).to receive(:fetch_metric).with(:counter, :gitlab_transaction_event_bau_total).and_return(prometheus_metric)
+
+ transaction_obj.add_event(:bau, animal: 'dog')
+ end
+
+ context 'with sensitive tags' do
+ it 'filters tags' do
+ expect(described_class).to receive(:fetch_metric).with(:counter, :gitlab_transaction_event_bau_total).and_return(prometheus_metric)
+ expect(prometheus_metric).not_to receive(:increment).with(hash_including(sensitive_tags))
+
+ transaction_obj.add_event(:bau, **sensitive_tags.merge(sane: 'yes'))
+ end
+ end
+ end
+
+ describe '#increment' do
+ let(:prometheus_metric) { instance_double(Prometheus::Client::Counter, increment: nil, base_labels: {}) }
+
+ it 'adds a metric' do
+ expect(::Gitlab::Metrics).to receive(:counter).with(
+ :meow, 'Meow counter', hash_including(*described_class::BASE_LABEL_KEYS)
+ ).and_return(prometheus_metric)
+ expect(prometheus_metric).to receive(:increment).with(labels, 1)
+
+ transaction_obj.increment(:meow, 1)
+ end
+
+ context 'with block' do
+ it 'overrides docstring' do
+ expect(::Gitlab::Metrics).to receive(:counter).with(
+ :block_docstring, 'test', hash_including(*described_class::BASE_LABEL_KEYS)
+ ).and_return(prometheus_metric)
+ expect(prometheus_metric).to receive(:increment).with(labels, 1)
+
+ transaction_obj.increment(:block_docstring, 1) do
+ docstring 'test'
+ end
+ end
+
+ it 'overrides labels' do
+ expect(::Gitlab::Metrics).to receive(:counter).with(
+ :block_labels, 'Block labels counter', hash_including(*described_class::BASE_LABEL_KEYS)
+ ).and_return(prometheus_metric)
+ expect(prometheus_metric).to receive(:increment).with(labels.merge(sane: 'yes'), 1)
+
+ transaction_obj.increment(:block_labels, 1, sane: 'yes') do
+ label_keys %i(sane)
+ end
+ end
+
+ it 'filters sensitive tags' do
+ labels_keys = sensitive_tags.keys
+
+ expect(::Gitlab::Metrics).to receive(:counter).with(
+ :metric_with_sensitive_block, 'Metric with sensitive block counter', hash_excluding(labels_keys)
+ ).and_return(prometheus_metric)
+ expect(prometheus_metric).to receive(:increment).with(labels, 1)
+
+ transaction_obj.increment(:metric_with_sensitive_block, 1, sensitive_tags) do
+ label_keys labels_keys
+ end
+ end
+ end
+ end
+
+ describe '#set' do
+ let(:prometheus_metric) { instance_double(Prometheus::Client::Gauge, set: nil, base_labels: {}) }
+
+ it 'adds a metric' do
+ expect(::Gitlab::Metrics).to receive(:gauge).with(
+ :meow_set, 'Meow set gauge', hash_including(*described_class::BASE_LABEL_KEYS), :all
+ ).and_return(prometheus_metric)
+ expect(prometheus_metric).to receive(:set).with(labels, 99)
+
+ transaction_obj.set(:meow_set, 99)
+ end
+
+ context 'with block' do
+ it 'overrides docstring' do
+ expect(::Gitlab::Metrics).to receive(:gauge).with(
+ :block_docstring_set, 'test', hash_including(*described_class::BASE_LABEL_KEYS), :all
+ ).and_return(prometheus_metric)
+ expect(prometheus_metric).to receive(:set).with(labels, 99)
+
+ transaction_obj.set(:block_docstring_set, 99) do
+ docstring 'test'
+ end
+ end
+
+ it 'overrides labels' do
+ expect(::Gitlab::Metrics).to receive(:gauge).with(
+ :block_labels_set, 'Block labels set gauge', hash_including(*described_class::BASE_LABEL_KEYS), :all
+ ).and_return(prometheus_metric)
+ expect(prometheus_metric).to receive(:set).with(labels.merge(sane: 'yes'), 99)
+
+ transaction_obj.set(:block_labels_set, 99, sane: 'yes') do
+ label_keys %i(sane)
+ end
+ end
+
+ it 'filters sensitive tags' do
+ labels_keys = sensitive_tags.keys
+
+ expect(::Gitlab::Metrics).to receive(:gauge).with(
+ :metric_set_with_sensitive_block, 'Metric set with sensitive block gauge', hash_excluding(*labels_keys), :all
+ ).and_return(prometheus_metric)
+ expect(prometheus_metric).to receive(:set).with(labels, 99)
+
+ transaction_obj.set(:metric_set_with_sensitive_block, 99, sensitive_tags) do
+ label_keys label_keys
+ end
+ end
+ end
+ end
+
+ describe '#observe' do
+ let(:prometheus_metric) { instance_double(Prometheus::Client::Histogram, observe: nil, base_labels: {}) }
+
+ it 'adds a metric' do
+ expect(::Gitlab::Metrics).to receive(:histogram).with(
+ :meow_observe, 'Meow observe histogram', hash_including(*described_class::BASE_LABEL_KEYS), kind_of(Array)
+ ).and_return(prometheus_metric)
+ expect(prometheus_metric).to receive(:observe).with(labels, 2.0)
+
+ transaction_obj.observe(:meow_observe, 2.0)
+ end
+
+ context 'with block' do
+ it 'overrides docstring' do
+ expect(::Gitlab::Metrics).to receive(:histogram).with(
+ :block_docstring_observe, 'test', hash_including(*described_class::BASE_LABEL_KEYS), kind_of(Array)
+ ).and_return(prometheus_metric)
+ expect(prometheus_metric).to receive(:observe).with(labels, 2.0)
+
+ transaction_obj.observe(:block_docstring_observe, 2.0) do
+ docstring 'test'
+ end
+ end
+
+ it 'overrides labels' do
+ expect(::Gitlab::Metrics).to receive(:histogram).with(
+ :block_labels_observe, 'Block labels observe histogram', hash_including(*described_class::BASE_LABEL_KEYS), kind_of(Array)
+ ).and_return(prometheus_metric)
+ expect(prometheus_metric).to receive(:observe).with(labels.merge(sane: 'yes'), 2.0)
+
+ transaction_obj.observe(:block_labels_observe, 2.0, sane: 'yes') do
+ label_keys %i(sane)
+ end
+ end
+
+ it 'filters sensitive tags' do
+ labels_keys = sensitive_tags.keys
+
+ expect(::Gitlab::Metrics).to receive(:histogram).with(
+ :metric_observe_with_sensitive_block,
+ 'Metric observe with sensitive block histogram',
+ hash_excluding(labels_keys),
+ kind_of(Array)
+ ).and_return(prometheus_metric)
+ expect(prometheus_metric).to receive(:observe).with(labels, 2.0)
+
+ transaction_obj.observe(:metric_observe_with_sensitive_block, 2.0, sensitive_tags) do
+ label_keys label_keys
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb b/spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb
index f928fb1eb43..d823e7ac221 100644
--- a/spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb
+++ b/spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb
@@ -12,6 +12,7 @@ RSpec.shared_examples 'StageEventModel' do
project_id: 4,
author_id: 5,
milestone_id: 6,
+ state_id: 1,
start_event_timestamp: time,
end_event_timestamp: time
},
@@ -22,6 +23,7 @@ RSpec.shared_examples 'StageEventModel' do
project_id: 11,
author_id: 12,
milestone_id: 13,
+ state_id: 1,
start_event_timestamp: time,
end_event_timestamp: time
}
@@ -34,8 +36,9 @@ RSpec.shared_examples 'StageEventModel' do
described_class.issuable_id_column,
:group_id,
:project_id,
- :milestone_id,
:author_id,
+ :milestone_id,
+ :state_id,
:start_event_timestamp,
:end_event_timestamp
]
@@ -59,10 +62,120 @@ RSpec.shared_examples 'StageEventModel' do
upsert_data
output_data = described_class.all.map do |record|
- column_order.map { |column| record[column] }
+ column_order.map do |column|
+ if column == :state_id
+ described_class.states[record[column]]
+ else
+ record[column]
+ end
+ end
end.sort
expect(input_data.map(&:values).sort).to eq(output_data)
end
end
+
+ describe 'scopes' do
+ def attributes(array)
+ array.map(&:attributes)
+ end
+
+ RSpec::Matchers.define :match_attributes do |expected|
+ match do |actual|
+ actual.map(&:attributes) == expected.map(&:attributes)
+ end
+ end
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:user) }
+ let_it_be(:milestone) { create(:milestone) }
+ let_it_be(:issuable_with_assignee) { create(issuable_factory, assignees: [user])}
+
+ let_it_be(:record) { create(stage_event_factory, start_event_timestamp: 3.years.ago.to_date, end_event_timestamp: 2.years.ago.to_date) }
+ let_it_be(:record_with_author) { create(stage_event_factory, author_id: user.id) }
+ let_it_be(:record_with_project) { create(stage_event_factory, project_id: project.id) }
+ let_it_be(:record_with_group) { create(stage_event_factory, group_id: project.namespace_id) }
+ let_it_be(:record_with_assigned_issuable) { create(stage_event_factory, described_class.issuable_id_column => issuable_with_assignee.id) }
+ let_it_be(:record_with_milestone) { create(stage_event_factory, milestone_id: milestone.id) }
+
+ it 'filters by stage_event_hash_id' do
+ records = described_class.by_stage_event_hash_id(record.stage_event_hash_id)
+
+ expect(records).to match_attributes([record])
+ end
+
+ it 'filters by project_id' do
+ records = described_class.by_project_id(project.id)
+
+ expect(records).to match_attributes([record_with_project])
+ end
+
+ it 'filters by group_id' do
+ records = described_class.by_group_id(project.namespace_id)
+
+ expect(records).to match_attributes([record_with_group])
+ end
+
+ it 'filters by author_id' do
+ records = described_class.authored(user)
+
+ expect(records).to match_attributes([record_with_author])
+ end
+
+ it 'filters by assignee' do
+ records = described_class.assigned_to(user)
+
+ expect(records).to match_attributes([record_with_assigned_issuable])
+ end
+
+ it 'filters by milestone_id' do
+ records = described_class.with_milestone_id(milestone.id)
+
+ expect(records).to match_attributes([record_with_milestone])
+ end
+
+ describe 'start_event_timestamp filtering' do
+ it 'when range is given' do
+ records = described_class
+ .start_event_timestamp_after(4.years.ago)
+ .start_event_timestamp_before(2.years.ago)
+
+ expect(records).to match_attributes([record])
+ end
+
+ it 'when specifying upper bound' do
+ records = described_class.start_event_timestamp_before(2.years.ago)
+
+ expect(attributes(records)).to include(attributes([record]).first)
+ end
+
+ it 'when specifying the lower bound' do
+ records = described_class.start_event_timestamp_after(4.years.ago)
+
+ expect(attributes(records)).to include(attributes([record]).first)
+ end
+ end
+
+ describe 'end_event_timestamp filtering' do
+ it 'when range is given' do
+ records = described_class
+ .end_event_timestamp_after(3.years.ago)
+ .end_event_timestamp_before(1.year.ago)
+
+ expect(records).to match_attributes([record])
+ end
+
+ it 'when specifying upper bound' do
+ records = described_class.end_event_timestamp_before(1.year.ago)
+
+ expect(attributes(records)).to include(attributes([record]).first)
+ end
+
+ it 'when specifying the lower bound' do
+ records = described_class.end_event_timestamp_after(3.years.ago)
+
+ expect(attributes(records)).to include(attributes([record]).first)
+ end
+ end
+ end
end
diff --git a/spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb b/spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb
index a4e0d6c871e..2d08de297a3 100644
--- a/spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb
@@ -11,18 +11,18 @@ RSpec.shared_examples 'ttl_expirable' do
it { is_expected.to validate_presence_of(:status) }
end
- describe '.updated_before' do
+ describe '.read_before' do
# rubocop:disable Rails/SaveBang
let_it_be_with_reload(:item1) { create(class_symbol) }
let_it_be(:item2) { create(class_symbol) }
# rubocop:enable Rails/SaveBang
before do
- item1.update_column(:updated_at, 1.month.ago)
+ item1.update_column(:read_at, 1.month.ago)
end
it 'returns items with created at older than the supplied number of days' do
- expect(described_class.updated_before(10)).to contain_exactly(item1)
+ expect(described_class.read_before(10)).to contain_exactly(item1)
end
end
@@ -48,4 +48,13 @@ RSpec.shared_examples 'ttl_expirable' do
expect(described_class.lock_next_by(:created_at)).to contain_exactly(item3)
end
end
+
+ describe '#read', :freeze_time do
+ let_it_be(:old_read_at) { 1.day.ago }
+ let_it_be(:item1) { create(class_symbol, read_at: old_read_at) }
+
+ it 'updates read_at' do
+ expect { item1.read! }.to change { item1.reload.read_at }
+ end
+ end
end
diff --git a/spec/support/shared_examples/models/member_shared_examples.rb b/spec/support/shared_examples/models/member_shared_examples.rb
index 56c202cb228..a2909c66e22 100644
--- a/spec/support/shared_examples/models/member_shared_examples.rb
+++ b/spec/support/shared_examples/models/member_shared_examples.rb
@@ -299,6 +299,22 @@ RSpec.shared_examples_for "member creation" do
end
end
end
+
+ context 'when `tasks_to_be_done` and `tasks_project_id` are passed' do
+ before do
+ stub_experiments(invite_members_for_task: true)
+ end
+
+ it 'creates a member_task with the correct attributes', :aggregate_failures do
+ task_project = source.is_a?(Group) ? create(:project, group: source) : source
+ described_class.new(source, user, :developer, tasks_to_be_done: %w(ci code), tasks_project_id: task_project.id).execute
+
+ member = source.members.last
+
+ expect(member.tasks_to_be_done).to match_array([:ci, :code])
+ expect(member.member_task.project).to eq(task_project)
+ end
+ end
end
end
@@ -379,5 +395,20 @@ RSpec.shared_examples_for "bulk member creation" do
expect(members).to all(be_persisted)
end
end
+
+ context 'when `tasks_to_be_done` and `tasks_project_id` are passed' do
+ before do
+ stub_experiments(invite_members_for_task: true)
+ end
+
+ it 'creates a member_task with the correct attributes', :aggregate_failures do
+ task_project = source.is_a?(Group) ? create(:project, group: source) : source
+ members = described_class.add_users(source, [user1], :developer, tasks_to_be_done: %w(ci code), tasks_project_id: task_project.id)
+ member = members.last
+
+ expect(member.tasks_to_be_done).to match_array([:ci, :code])
+ expect(member.member_task.project).to eq(task_project)
+ end
+ end
end
end
diff --git a/spec/support/shared_examples/models/reviewer_state_shared_examples.rb b/spec/support/shared_examples/models/reviewer_state_shared_examples.rb
new file mode 100644
index 00000000000..f1392768b06
--- /dev/null
+++ b/spec/support/shared_examples/models/reviewer_state_shared_examples.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'having reviewer state' do
+ describe 'mr_attention_requests feature flag is disabled' do
+ before do
+ stub_feature_flags(mr_attention_requests: false)
+ end
+
+ it { is_expected.to have_attributes(state: 'unreviewed') }
+ end
+
+ describe 'mr_attention_requests feature flag is enabled' do
+ it { is_expected.to have_attributes(state: 'attention_requested') }
+ end
+end
diff --git a/spec/support/shared_examples/namespaces/traversal_examples.rb b/spec/support/shared_examples/namespaces/traversal_examples.rb
index d126b242fb0..ac6a843663f 100644
--- a/spec/support/shared_examples/namespaces/traversal_examples.rb
+++ b/spec/support/shared_examples/namespaces/traversal_examples.rb
@@ -22,6 +22,8 @@ RSpec.shared_examples 'namespace traversal' do
let_it_be(:deep_nested_group) { create(:group, parent: nested_group) }
let_it_be(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
let_it_be(:groups) { [group, nested_group, deep_nested_group, very_deep_nested_group] }
+ let_it_be(:project) { create(:project, group: nested_group) }
+ let_it_be(:project_namespace) { project.project_namespace }
describe '#root_ancestor' do
it 'returns the correct root ancestor' do
@@ -65,6 +67,7 @@ RSpec.shared_examples 'namespace traversal' do
expect(deep_nested_group.ancestors).to contain_exactly(group, nested_group)
expect(nested_group.ancestors).to contain_exactly(group)
expect(group.ancestors).to eq([])
+ expect(project_namespace.ancestors).to be_empty
end
context 'with asc hierarchy_order' do
@@ -73,6 +76,7 @@ RSpec.shared_examples 'namespace traversal' do
expect(deep_nested_group.ancestors(hierarchy_order: :asc)).to eq [nested_group, group]
expect(nested_group.ancestors(hierarchy_order: :asc)).to eq [group]
expect(group.ancestors(hierarchy_order: :asc)).to eq([])
+ expect(project_namespace.ancestors(hierarchy_order: :asc)).to be_empty
end
end
@@ -82,6 +86,7 @@ RSpec.shared_examples 'namespace traversal' do
expect(deep_nested_group.ancestors(hierarchy_order: :desc)).to eq [group, nested_group]
expect(nested_group.ancestors(hierarchy_order: :desc)).to eq [group]
expect(group.ancestors(hierarchy_order: :desc)).to eq([])
+ expect(project_namespace.ancestors(hierarchy_order: :desc)).to be_empty
end
end
@@ -98,6 +103,7 @@ RSpec.shared_examples 'namespace traversal' do
expect(deep_nested_group.ancestor_ids).to contain_exactly(group.id, nested_group.id)
expect(nested_group.ancestor_ids).to contain_exactly(group.id)
expect(group.ancestor_ids).to be_empty
+ expect(project_namespace.ancestor_ids).to be_empty
end
context 'with asc hierarchy_order' do
@@ -106,6 +112,7 @@ RSpec.shared_examples 'namespace traversal' do
expect(deep_nested_group.ancestor_ids(hierarchy_order: :asc)).to eq [nested_group.id, group.id]
expect(nested_group.ancestor_ids(hierarchy_order: :asc)).to eq [group.id]
expect(group.ancestor_ids(hierarchy_order: :asc)).to eq([])
+ expect(project_namespace.ancestor_ids(hierarchy_order: :asc)).to eq([])
end
end
@@ -115,6 +122,7 @@ RSpec.shared_examples 'namespace traversal' do
expect(deep_nested_group.ancestor_ids(hierarchy_order: :desc)).to eq [group.id, nested_group.id]
expect(nested_group.ancestor_ids(hierarchy_order: :desc)).to eq [group.id]
expect(group.ancestor_ids(hierarchy_order: :desc)).to eq([])
+ expect(project_namespace.ancestor_ids(hierarchy_order: :desc)).to eq([])
end
end
@@ -131,6 +139,7 @@ RSpec.shared_examples 'namespace traversal' do
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)
+ expect(project_namespace.self_and_ancestors).to contain_exactly(project_namespace)
end
context 'with asc hierarchy_order' do
@@ -139,6 +148,7 @@ RSpec.shared_examples 'namespace traversal' do
expect(deep_nested_group.self_and_ancestors(hierarchy_order: :asc)).to eq [deep_nested_group, nested_group, group]
expect(nested_group.self_and_ancestors(hierarchy_order: :asc)).to eq [nested_group, group]
expect(group.self_and_ancestors(hierarchy_order: :asc)).to eq([group])
+ expect(project_namespace.self_and_ancestors(hierarchy_order: :asc)).to eq([project_namespace])
end
end
@@ -148,6 +158,7 @@ RSpec.shared_examples 'namespace traversal' do
expect(deep_nested_group.self_and_ancestors(hierarchy_order: :desc)).to eq [group, nested_group, deep_nested_group]
expect(nested_group.self_and_ancestors(hierarchy_order: :desc)).to eq [group, nested_group]
expect(group.self_and_ancestors(hierarchy_order: :desc)).to eq([group])
+ expect(project_namespace.self_and_ancestors(hierarchy_order: :desc)).to eq([project_namespace])
end
end
@@ -164,6 +175,7 @@ RSpec.shared_examples 'namespace traversal' do
expect(deep_nested_group.self_and_ancestor_ids).to contain_exactly(group.id, nested_group.id, deep_nested_group.id)
expect(nested_group.self_and_ancestor_ids).to contain_exactly(group.id, nested_group.id)
expect(group.self_and_ancestor_ids).to contain_exactly(group.id)
+ expect(project_namespace.self_and_ancestor_ids).to contain_exactly(project_namespace.id)
end
context 'with asc hierarchy_order' do
@@ -172,6 +184,7 @@ RSpec.shared_examples 'namespace traversal' do
expect(deep_nested_group.self_and_ancestor_ids(hierarchy_order: :asc)).to eq [deep_nested_group.id, nested_group.id, group.id]
expect(nested_group.self_and_ancestor_ids(hierarchy_order: :asc)).to eq [nested_group.id, group.id]
expect(group.self_and_ancestor_ids(hierarchy_order: :asc)).to eq([group.id])
+ expect(project_namespace.self_and_ancestor_ids(hierarchy_order: :asc)).to eq([project_namespace.id])
end
end
@@ -181,6 +194,7 @@ RSpec.shared_examples 'namespace traversal' do
expect(deep_nested_group.self_and_ancestor_ids(hierarchy_order: :desc)).to eq [group.id, nested_group.id, deep_nested_group.id]
expect(nested_group.self_and_ancestor_ids(hierarchy_order: :desc)).to eq [group.id, nested_group.id]
expect(group.self_and_ancestor_ids(hierarchy_order: :desc)).to eq([group.id])
+ expect(project_namespace.self_and_ancestor_ids(hierarchy_order: :desc)).to eq([project_namespace.id])
end
end
@@ -205,6 +219,10 @@ RSpec.shared_examples 'namespace traversal' do
describe '#recursive_descendants' do
it_behaves_like 'recursive version', :descendants
end
+
+ it 'does not include project namespaces' do
+ expect(group.descendants.to_a).not_to include(project_namespace)
+ end
end
describe '#self_and_descendants' do
@@ -223,6 +241,10 @@ RSpec.shared_examples 'namespace traversal' do
it_behaves_like 'recursive version', :self_and_descendants
end
+
+ it 'does not include project namespaces' do
+ expect(group.self_and_descendants.to_a).not_to include(project_namespace)
+ end
end
describe '#self_and_descendant_ids' do
diff --git a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
index 74b1bacc560..4c09c1c2a3b 100644
--- a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
+++ b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
@@ -25,12 +25,6 @@ RSpec.shared_examples 'namespace traversal scopes' do
it { is_expected.to contain_exactly(group_1.id, group_2.id) }
end
- describe '.without_sti_condition' do
- subject { described_class.without_sti_condition }
-
- it { expect(subject.where_values_hash).not_to have_key(:type) }
- end
-
describe '.order_by_depth' do
subject { described_class.where(id: [group_1, nested_group_1, deep_nested_group_1]).order_by_depth(direction) }
@@ -55,6 +49,53 @@ RSpec.shared_examples 'namespace traversal scopes' do
it { is_expected.to eq described_class.column_names }
end
+ shared_examples '.roots' do
+ context 'with only sub-groups' do
+ subject { described_class.where(id: [deep_nested_group_1, nested_group_1, deep_nested_group_2]).roots }
+
+ it { is_expected.to contain_exactly(group_1, group_2) }
+ end
+
+ context 'with only root groups' do
+ subject { described_class.where(id: [group_1, group_2]).roots }
+
+ it { is_expected.to contain_exactly(group_1, group_2) }
+ end
+
+ context 'with all groups' do
+ subject { described_class.where(id: groups).roots }
+
+ it { is_expected.to contain_exactly(group_1, group_2) }
+ end
+ end
+
+ describe '.roots' do
+ context "use_traversal_ids_roots feature flag is true" do
+ before do
+ stub_feature_flags(use_traversal_ids: true)
+ stub_feature_flags(use_traversal_ids_roots: true)
+ end
+
+ it_behaves_like '.roots'
+
+ it 'not make recursive queries' do
+ expect { described_class.where(id: [nested_group_1]).roots.load }.not_to make_queries_matching(/WITH RECURSIVE/)
+ end
+ end
+
+ context "use_traversal_ids_roots feature flag is false" do
+ before do
+ stub_feature_flags(use_traversal_ids_roots: false)
+ end
+
+ it_behaves_like '.roots'
+
+ it 'make recursive queries' do
+ expect { described_class.where(id: [nested_group_1]).roots.load }.to make_queries_matching(/WITH RECURSIVE/)
+ end
+ end
+ end
+
shared_examples '.self_and_ancestors' do
subject { described_class.where(id: [nested_group_1, nested_group_2]).self_and_ancestors }
@@ -156,7 +197,7 @@ RSpec.shared_examples 'namespace traversal scopes' do
end
end
- describe '.self_and_descendants' do
+ shared_examples '.self_and_descendants' do
subject { described_class.where(id: [nested_group_1, nested_group_2]).self_and_descendants }
it { is_expected.to contain_exactly(nested_group_1, deep_nested_group_1, nested_group_2, deep_nested_group_2) }
@@ -174,7 +215,19 @@ RSpec.shared_examples 'namespace traversal scopes' do
end
end
- describe '.self_and_descendant_ids' do
+ describe '.self_and_descendants' do
+ include_examples '.self_and_descendants'
+
+ context 'with traversal_ids_btree feature flag disabled' do
+ before do
+ stub_feature_flags(traversal_ids_btree: false)
+ end
+
+ include_examples '.self_and_descendants'
+ end
+ end
+
+ shared_examples '.self_and_descendant_ids' do
subject { described_class.where(id: [nested_group_1, nested_group_2]).self_and_descendant_ids.pluck(:id) }
it { is_expected.to contain_exactly(nested_group_1.id, deep_nested_group_1.id, nested_group_2.id, deep_nested_group_2.id) }
@@ -190,4 +243,16 @@ RSpec.shared_examples 'namespace traversal scopes' do
it { is_expected.to contain_exactly(deep_nested_group_1.id, deep_nested_group_2.id) }
end
end
+
+ describe '.self_and_descendant_ids' do
+ include_examples '.self_and_descendant_ids'
+
+ context 'with traversal_ids_btree feature flag disabled' do
+ before do
+ stub_feature_flags(traversal_ids_btree: false)
+ end
+
+ include_examples '.self_and_descendant_ids'
+ end
+ end
end
diff --git a/spec/support/shared_examples/quick_actions/issue/promote_to_incident_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/promote_to_incident_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..5167d27f8b9
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issue/promote_to_incident_quick_action_shared_examples.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'promote_to_incident quick action' do
+ describe '/promote_to_incident' do
+ context 'when issue can be promoted' do
+ it 'promotes issue to incident' do
+ add_note('/promote_to_incident')
+
+ expect(issue.reload.issue_type).to eq('incident')
+ expect(page).to have_content('Issue has been promoted to incident')
+ end
+ end
+
+ context 'when issue is already an incident' do
+ let(:issue) { create(:incident, project: project) }
+
+ it 'does not promote the issue' do
+ add_note('/promote_to_incident')
+
+ expect(page).to have_content('Could not apply promote_to_incident command')
+ end
+ end
+
+ context 'when user does not have permissions' do
+ let(:guest) { create(:user) }
+
+ before do
+ sign_in(guest)
+ visit project_issue_path(project, issue)
+ wait_for_all_requests
+ end
+
+ it 'does not promote the issue' do
+ add_note('/promote_to_incident')
+
+ expect(page).to have_content('Could not apply promote_to_incident command')
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/debian_common_shared_examples.rb b/spec/support/shared_examples/requests/api/debian_common_shared_examples.rb
new file mode 100644
index 00000000000..e0225070986
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/debian_common_shared_examples.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'rejects Debian access with unknown container id' do |anonymous_status, auth_method|
+ context 'with an unknown container' do
+ let(:container) { double(id: non_existing_record_id) }
+
+ context 'as anonymous' do
+ it_behaves_like 'Debian packages GET request', anonymous_status, nil
+ end
+
+ context 'as authenticated user' do
+ include_context 'Debian repository auth headers', :not_a_member, auth_method do
+ it_behaves_like 'Debian packages GET request', :not_found, nil
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/debian_distributions_shared_examples.rb b/spec/support/shared_examples/requests/api/debian_distributions_shared_examples.rb
new file mode 100644
index 00000000000..5cd63c33936
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/debian_distributions_shared_examples.rb
@@ -0,0 +1,192 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'Debian distributions GET request' do |status, body = nil|
+ and_body = body.nil? ? '' : ' and expected body'
+
+ it "returns #{status}#{and_body}" do
+ subject
+
+ expect(response).to have_gitlab_http_status(status)
+
+ unless body.nil?
+ expect(response.body).to match(body)
+ end
+ end
+end
+
+RSpec.shared_examples 'Debian distributions PUT request' do |status, body|
+ and_body = body.nil? ? '' : ' and expected body'
+
+ if status == :success
+ it 'updates distribution', :aggregate_failures do
+ expect(::Packages::Debian::UpdateDistributionService).to receive(:new).with(distribution, api_params.except(:codename)).and_call_original
+
+ expect { subject }
+ .to not_change { Packages::Debian::GroupDistribution.all.count + Packages::Debian::ProjectDistribution.all.count }
+ .and not_change { Packages::Debian::GroupComponent.all.count + Packages::Debian::ProjectComponent.all.count }
+ .and not_change { Packages::Debian::GroupArchitecture.all.count + Packages::Debian::ProjectArchitecture.all.count }
+
+ expect(response).to have_gitlab_http_status(status)
+ expect(response.media_type).to eq('application/json')
+
+ unless body.nil?
+ expect(response.body).to match(body)
+ end
+ end
+ else
+ it "returns #{status}#{and_body}", :aggregate_failures do
+ subject
+
+ expect(response).to have_gitlab_http_status(status)
+
+ unless body.nil?
+ expect(response.body).to match(body)
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'Debian distributions DELETE request' do |status, body|
+ and_body = body.nil? ? '' : ' and expected body'
+
+ if status == :success
+ it 'updates distribution', :aggregate_failures do
+ expect { subject }
+ .to change { Packages::Debian::GroupDistribution.all.count + Packages::Debian::ProjectDistribution.all.count }.by(-1)
+ .and change { Packages::Debian::GroupComponent.all.count + Packages::Debian::ProjectComponent.all.count }.by(-1)
+ .and change { Packages::Debian::GroupArchitecture.all.count + Packages::Debian::ProjectArchitecture.all.count }.by(-2)
+
+ expect(response).to have_gitlab_http_status(status)
+ expect(response.media_type).to eq('application/json')
+
+ unless body.nil?
+ expect(response.body).to match(body)
+ end
+ end
+ else
+ it "returns #{status}#{and_body}", :aggregate_failures do
+ subject
+
+ expect(response).to have_gitlab_http_status(status)
+
+ unless body.nil?
+ expect(response.body).to match(body)
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'Debian distributions POST request' do |status, body|
+ and_body = body.nil? ? '' : ' and expected body'
+
+ if status == :created
+ it 'creates distribution', :aggregate_failures do
+ expect(::Packages::Debian::CreateDistributionService).to receive(:new).with(container, user, api_params).and_call_original
+
+ expect { subject }
+ .to change { Packages::Debian::GroupDistribution.all.count + Packages::Debian::ProjectDistribution.all.count }.by(1)
+ .and change { Packages::Debian::GroupComponent.all.count + Packages::Debian::ProjectComponent.all.count }.by(1)
+ .and change { Packages::Debian::GroupArchitecture.all.count + Packages::Debian::ProjectArchitecture.all.count }.by(2)
+
+ expect(response).to have_gitlab_http_status(status)
+ expect(response.media_type).to eq('application/json')
+
+ unless body.nil?
+ expect(response.body).to match(body)
+ end
+ end
+ else
+ it "returns #{status}#{and_body}", :aggregate_failures do
+ subject
+
+ expect(response).to have_gitlab_http_status(status)
+
+ unless body.nil?
+ expect(response.body).to match(body)
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'Debian distributions read endpoint' do |desired_behavior, success_status, success_body|
+ context 'with valid container' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:visibility_level, :user_type, :auth_method, :expected_status, :expected_body) do
+ :public | :guest | :private_token | success_status | success_body
+ :public | :not_a_member | :private_token | success_status | success_body
+ :public | :anonymous | :private_token | success_status | success_body
+ :public | :invalid_token | :private_token | :unauthorized | nil
+ :private | :developer | :private_token | success_status | success_body
+ :private | :developer | :basic | :not_found | nil
+ :private | :guest | :private_token | :forbidden | nil
+ :private | :not_a_member | :private_token | :not_found | nil
+ :private | :anonymous | :private_token | :not_found | nil
+ :private | :invalid_token | :private_token | :unauthorized | nil
+ end
+
+ with_them do
+ include_context 'Debian repository access', params[:visibility_level], params[:user_type], params[:auth_method] do
+ it_behaves_like "Debian distributions #{desired_behavior} request", params[:expected_status], params[:expected_body]
+ end
+ end
+ end
+
+ it_behaves_like 'rejects Debian access with unknown container id', :not_found, :private_token
+end
+
+RSpec.shared_examples 'Debian distributions write endpoint' do |desired_behavior, success_status, success_body|
+ context 'with valid container' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:visibility_level, :user_type, :auth_method, :expected_status, :expected_body) do
+ :public | :developer | :private_token | success_status | success_body
+ :public | :developer | :basic | :unauthorized | nil
+ :public | :guest | :private_token | :forbidden | nil
+ :public | :not_a_member | :private_token | :forbidden | nil
+ :public | :anonymous | :private_token | :unauthorized | nil
+ :public | :invalid_token | :private_token | :unauthorized | nil
+ :private | :developer | :private_token | success_status | success_body
+ :private | :guest | :private_token | :forbidden | nil
+ :private | :not_a_member | :private_token | :not_found | nil
+ :private | :anonymous | :private_token | :not_found | nil
+ :private | :invalid_token | :private_token | :unauthorized | nil
+ end
+
+ with_them do
+ include_context 'Debian repository access', params[:visibility_level], params[:user_type], params[:auth_method] do
+ it_behaves_like "Debian distributions #{desired_behavior} request", params[:expected_status], params[:expected_body]
+ end
+ end
+ end
+
+ it_behaves_like 'rejects Debian access with unknown container id', :not_found, :private_token
+end
+
+RSpec.shared_examples 'Debian distributions maintainer write endpoint' do |desired_behavior, success_status, success_body|
+ context 'with valid container' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:visibility_level, :user_type, :auth_method, :expected_status, :expected_body) do
+ :public | :maintainer | :private_token | success_status | success_body
+ :public | :maintainer | :basic | :unauthorized | nil
+ :public | :developer | :private_token | :forbidden | nil
+ :public | :not_a_member | :private_token | :forbidden | nil
+ :public | :anonymous | :private_token | :unauthorized | nil
+ :public | :invalid_token | :private_token | :unauthorized | nil
+ :private | :maintainer | :private_token | success_status | success_body
+ :private | :developer | :private_token | :forbidden | nil
+ :private | :not_a_member | :private_token | :not_found | nil
+ :private | :anonymous | :private_token | :not_found | nil
+ :private | :invalid_token | :private_token | :unauthorized | nil
+ end
+
+ with_them do
+ include_context 'Debian repository access', params[:visibility_level], params[:user_type], params[:auth_method] do
+ it_behaves_like "Debian distributions #{desired_behavior} request", params[:expected_status], params[:expected_body]
+ end
+ end
+ end
+
+ it_behaves_like 'rejects Debian access with unknown container id', :not_found, :private_token
+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 a3ed74085fb..2fd5e6a5f91 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,127 +1,6 @@
# frozen_string_literal: true
-RSpec.shared_context 'Debian repository shared context' do |container_type, can_freeze|
- include_context 'workhorse headers'
-
- before do
- stub_feature_flags(debian_packages: true, debian_group_packages: true)
- end
-
- let_it_be(:private_container, freeze: can_freeze) { create(container_type, :private) }
- let_it_be(:public_container, freeze: can_freeze) { create(container_type, :public) }
- let_it_be(:user, freeze: true) { create(:user) }
- let_it_be(:personal_access_token, freeze: true) { create(:personal_access_token, user: user) }
-
- let_it_be(:private_distribution, freeze: true) { create("debian_#{container_type}_distribution", :with_file, container: private_container, codename: 'existing-codename') }
- let_it_be(:private_component, freeze: true) { create("debian_#{container_type}_component", distribution: private_distribution, name: 'existing-component') }
- let_it_be(:private_architecture_all, freeze: true) { create("debian_#{container_type}_architecture", distribution: private_distribution, name: 'all') }
- let_it_be(:private_architecture, freeze: true) { create("debian_#{container_type}_architecture", distribution: private_distribution, name: 'existing-arch') }
- let_it_be(:private_component_file) { create("debian_#{container_type}_component_file", component: private_component, architecture: private_architecture) }
-
- let_it_be(:public_distribution, freeze: true) { create("debian_#{container_type}_distribution", :with_file, container: public_container, codename: 'existing-codename') }
- let_it_be(:public_component, freeze: true) { create("debian_#{container_type}_component", distribution: public_distribution, name: 'existing-component') }
- let_it_be(:public_architecture_all, freeze: true) { create("debian_#{container_type}_architecture", distribution: public_distribution, name: 'all') }
- let_it_be(:public_architecture, freeze: true) { create("debian_#{container_type}_architecture", distribution: public_distribution, name: 'existing-arch') }
- let_it_be(:public_component_file) { create("debian_#{container_type}_component_file", component: public_component, architecture: public_architecture) }
-
- if container_type == :group
- let_it_be(:private_project) { create(:project, :private, group: private_container) }
- let_it_be(:public_project) { create(:project, :public, group: public_container) }
- let_it_be(:private_project_distribution) { create(:debian_project_distribution, container: private_project, codename: 'existing-codename') }
- let_it_be(:public_project_distribution) { create(:debian_project_distribution, container: public_project, codename: 'existing-codename') }
-
- let(:project) { { private: private_project, public: public_project }[visibility_level] }
- else
- let_it_be(:private_project) { private_container }
- let_it_be(:public_project) { public_container }
- let_it_be(:private_project_distribution) { private_distribution }
- let_it_be(:public_project_distribution) { public_distribution }
- end
-
- let_it_be(:private_package) { create(:debian_package, project: private_project, published_in: private_project_distribution) }
- let_it_be(:public_package) { create(:debian_package, project: public_project, published_in: public_project_distribution) }
-
- let(:visibility_level) { :public }
-
- let(:distribution) { { private: private_distribution, public: public_distribution }[visibility_level] }
- let(:architecture) { { private: private_architecture, public: public_architecture }[visibility_level] }
- let(:component) { { private: private_component, public: public_component }[visibility_level] }
- let(:component_file) { { private: private_component_file, public: public_component_file }[visibility_level] }
- let(:package) { { private: private_package, public: public_package }[visibility_level] }
- let(:letter) { package.name[0..2] == 'lib' ? package.name[0..3] : package.name[0] }
-
- let(:method) { :get }
-
- let(:workhorse_params) do
- if method == :put
- file_upload = fixture_file_upload("spec/fixtures/packages/debian/#{file_name}")
- { file: file_upload }
- else
- {}
- end
- end
-
- let(:api_params) { workhorse_params }
-
- let(:auth_headers) { {} }
- let(:wh_headers) do
- if method == :put
- workhorse_headers
- else
- {}
- end
- end
-
- let(:headers) { auth_headers.merge(wh_headers) }
-
- let(:send_rewritten_field) { true }
-
- subject do
- if method == :put
- workhorse_finalize(
- api(url),
- method: method,
- file_key: :file,
- params: api_params,
- headers: headers,
- send_rewritten_field: send_rewritten_field
- )
- else
- send method, api(url), headers: headers, params: api_params
- end
- end
-end
-
-RSpec.shared_context 'with file_name' do |file_name|
- let(:file_name) { file_name }
-end
-
-RSpec.shared_context 'Debian repository auth headers' do |user_role, user_token, auth_method = :token|
- let(:token) { user_token ? personal_access_token.token : 'wrong' }
-
- let(:auth_headers) do
- if user_role == :anonymous
- {}
- elsif auth_method == :token
- { 'Private-Token' => token }
- else
- basic_auth_header(user.username, token)
- end
- end
-end
-
-RSpec.shared_context 'Debian repository access' do |visibility_level, user_role, add_member, user_token, auth_method|
- include_context 'Debian repository auth headers', user_role, user_token, auth_method do
- let(:containers) { { private: private_container, public: public_container } }
- let(:container) { containers[visibility_level] }
-
- before do
- container.send("add_#{user_role}", user) if add_member && user_role != :anonymous
- end
- end
-end
-
-RSpec.shared_examples 'Debian repository GET request' do |status, body = nil|
+RSpec.shared_examples 'Debian packages GET request' do |status, body = nil|
and_body = body.nil? ? '' : ' and expected body'
it "returns #{status}#{and_body}" do
@@ -135,7 +14,7 @@ RSpec.shared_examples 'Debian repository GET request' do |status, body = nil|
end
end
-RSpec.shared_examples 'Debian repository upload request' do |status, body = nil|
+RSpec.shared_examples 'Debian packages upload request' do |status, body = nil|
and_body = body.nil? ? '' : ' and expected body'
if status == :created
@@ -175,7 +54,7 @@ RSpec.shared_examples 'Debian repository upload request' do |status, body = nil|
end
end
-RSpec.shared_examples 'Debian repository upload authorize request' do |status, body = nil|
+RSpec.shared_examples 'Debian packages upload authorize request' do |status, body = nil|
and_body = body.nil? ? '' : ' and expected body'
if status == :created
@@ -221,237 +100,57 @@ RSpec.shared_examples 'Debian repository upload authorize request' do |status, b
end
end
-RSpec.shared_examples 'Debian repository POST distribution request' do |status, body|
- and_body = body.nil? ? '' : ' and expected body'
-
- if status == :created
- it 'creates distribution', :aggregate_failures do
- expect(::Packages::Debian::CreateDistributionService).to receive(:new).with(container, user, api_params).and_call_original
-
- expect { subject }
- .to change { Packages::Debian::GroupDistribution.all.count + Packages::Debian::ProjectDistribution.all.count }.by(1)
- .and change { Packages::Debian::GroupComponent.all.count + Packages::Debian::ProjectComponent.all.count }.by(1)
- .and change { Packages::Debian::GroupArchitecture.all.count + Packages::Debian::ProjectArchitecture.all.count }.by(2)
-
- expect(response).to have_gitlab_http_status(status)
- expect(response.media_type).to eq('application/json')
-
- unless body.nil?
- expect(response.body).to match(body)
- end
- end
- else
- it "returns #{status}#{and_body}", :aggregate_failures do
- subject
-
- expect(response).to have_gitlab_http_status(status)
-
- unless body.nil?
- expect(response.body).to match(body)
- end
- end
- end
-end
-
-RSpec.shared_examples 'Debian repository PUT distribution request' do |status, body|
- and_body = body.nil? ? '' : ' and expected body'
-
- if status == :success
- it 'updates distribution', :aggregate_failures do
- expect(::Packages::Debian::UpdateDistributionService).to receive(:new).with(distribution, api_params.except(:codename)).and_call_original
-
- expect { subject }
- .to not_change { Packages::Debian::GroupDistribution.all.count + Packages::Debian::ProjectDistribution.all.count }
- .and not_change { Packages::Debian::GroupComponent.all.count + Packages::Debian::ProjectComponent.all.count }
- .and not_change { Packages::Debian::GroupArchitecture.all.count + Packages::Debian::ProjectArchitecture.all.count }
-
- expect(response).to have_gitlab_http_status(status)
- expect(response.media_type).to eq('application/json')
-
- unless body.nil?
- expect(response.body).to match(body)
- end
- end
- else
- it "returns #{status}#{and_body}", :aggregate_failures do
- subject
-
- expect(response).to have_gitlab_http_status(status)
-
- unless body.nil?
- expect(response.body).to match(body)
- end
- end
- end
-end
-
-RSpec.shared_examples 'Debian repository DELETE distribution request' do |status, body|
- and_body = body.nil? ? '' : ' and expected body'
-
- if status == :success
- it 'updates distribution', :aggregate_failures do
- expect { subject }
- .to change { Packages::Debian::GroupDistribution.all.count + Packages::Debian::ProjectDistribution.all.count }.by(-1)
- .and change { Packages::Debian::GroupComponent.all.count + Packages::Debian::ProjectComponent.all.count }.by(-1)
- .and change { Packages::Debian::GroupArchitecture.all.count + Packages::Debian::ProjectArchitecture.all.count }.by(-2)
-
- expect(response).to have_gitlab_http_status(status)
- expect(response.media_type).to eq('application/json')
-
- unless body.nil?
- expect(response.body).to match(body)
- end
- end
- else
- it "returns #{status}#{and_body}", :aggregate_failures do
- subject
-
- expect(response).to have_gitlab_http_status(status)
-
- unless body.nil?
- expect(response.body).to match(body)
- end
- end
- end
-end
-
-RSpec.shared_examples 'rejects Debian access with unknown container id' do |hidden_status|
- context 'with an unknown container' do
- let(:container) { double(id: non_existing_record_id) }
-
- context 'as anonymous' do
- it_behaves_like 'Debian repository GET request', hidden_status, nil
- end
-
- context 'as authenticated user' do
- subject { get api(url), headers: basic_auth_header(user.username, personal_access_token.token) }
-
- it_behaves_like 'Debian repository GET request', :not_found, nil
- end
- end
-end
-
-RSpec.shared_examples 'Debian repository read endpoint' do |desired_behavior, success_status, success_body, authenticate_non_public: true|
- hidden_status = if authenticate_non_public
- :unauthorized
- else
- :not_found
- end
-
- context 'with valid container' do
- using RSpec::Parameterized::TableSyntax
-
- where(:visibility_level, :user_role, :member, :user_token, :expected_status, :expected_body) do
- :public | :developer | true | true | success_status | success_body
- :public | :guest | true | true | success_status | success_body
- :public | :developer | true | false | :unauthorized | nil
- :public | :guest | true | false | :unauthorized | nil
- :public | :developer | false | true | success_status | success_body
- :public | :guest | false | true | success_status | success_body
- :public | :developer | false | false | :unauthorized | nil
- :public | :guest | false | false | :unauthorized | nil
- :public | :anonymous | false | true | success_status | success_body
- :private | :developer | true | true | success_status | success_body
- :private | :guest | true | true | :forbidden | nil
- :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 | :unauthorized | nil
- :private | :guest | false | false | :unauthorized | nil
- :private | :anonymous | false | true | hidden_status | nil
- end
-
- with_them do
- include_context 'Debian repository access', params[:visibility_level], params[:user_role], params[:member], params[:user_token], :basic do
- it_behaves_like "Debian repository #{desired_behavior}", params[:expected_status], params[:expected_body]
- end
- end
- end
-
- it_behaves_like 'rejects Debian access with unknown container id', hidden_status
-end
-
-RSpec.shared_examples 'Debian repository write endpoint' do |desired_behavior, success_status, success_body, authenticate_non_public: true|
- hidden_status = if authenticate_non_public
- :unauthorized
- else
- :not_found
- end
-
+RSpec.shared_examples 'Debian packages read endpoint' do |desired_behavior, success_status, success_body|
context 'with valid container' do
using RSpec::Parameterized::TableSyntax
- where(:visibility_level, :user_role, :member, :user_token, :expected_status, :expected_body) do
- :public | :developer | true | true | success_status | success_body
- :public | :guest | true | true | :forbidden | nil
- :public | :developer | true | false | :unauthorized | nil
- :public | :guest | true | false | :unauthorized | nil
- :public | :developer | false | true | :forbidden | nil
- :public | :guest | false | true | :forbidden | nil
- :public | :developer | false | false | :unauthorized | nil
- :public | :guest | false | false | :unauthorized | nil
- :public | :anonymous | false | true | :unauthorized | nil
- :private | :developer | true | true | success_status | success_body
- :private | :guest | true | true | :forbidden | 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 | :unauthorized | nil
- :private | :guest | false | false | :unauthorized | nil
- :private | :anonymous | false | true | hidden_status | nil
+ where(:visibility_level, :user_type, :auth_method, :expected_status, :expected_body) do
+ :public | :guest | :basic | success_status | success_body
+ :public | :not_a_member | :basic | success_status | success_body
+ :public | :anonymous | :basic | success_status | success_body
+ :public | :invalid_token | :basic | :unauthorized | nil
+ :private | :developer | :basic | success_status | success_body
+ :private | :developer | :private_token | :unauthorized | nil
+ :private | :guest | :basic | :forbidden | nil
+ :private | :not_a_member | :basic | :not_found | nil
+ :private | :anonymous | :basic | :unauthorized | nil
+ :private | :invalid_token | :basic | :unauthorized | nil
end
with_them do
- include_context 'Debian repository access', params[:visibility_level], params[:user_role], params[:member], params[:user_token], :basic do
- it_behaves_like "Debian repository #{desired_behavior}", params[:expected_status], params[:expected_body]
+ include_context 'Debian repository access', params[:visibility_level], params[:user_type], params[:auth_method] do
+ it_behaves_like "Debian packages #{desired_behavior} request", params[:expected_status], params[:expected_body]
end
end
end
- it_behaves_like 'rejects Debian access with unknown container id', hidden_status
+ it_behaves_like 'rejects Debian access with unknown container id', :unauthorized, :basic
end
-RSpec.shared_examples 'Debian repository maintainer write endpoint' do |desired_behavior, success_status, success_body, authenticate_non_public: true|
- hidden_status = if authenticate_non_public
- :unauthorized
- else
- :not_found
- end
-
+RSpec.shared_examples 'Debian packages write endpoint' do |desired_behavior, success_status, success_body|
context 'with valid container' do
using RSpec::Parameterized::TableSyntax
- where(:visibility_level, :user_role, :member, :user_token, :expected_status, :expected_body) do
- :public | :maintainer | true | true | success_status | success_body
- :public | :developer | true | true | :forbidden | nil
- :public | :guest | true | true | :forbidden | nil
- :public | :maintainer | true | false | :unauthorized | nil
- :public | :guest | true | false | :unauthorized | nil
- :public | :maintainer | false | true | :forbidden | nil
- :public | :guest | false | true | :forbidden | nil
- :public | :maintainer | false | false | :unauthorized | nil
- :public | :guest | false | false | :unauthorized | nil
- :public | :anonymous | false | true | :unauthorized | nil
- :private | :maintainer | true | true | success_status | success_body
- :private | :developer | true | true | :forbidden | nil
- :private | :guest | true | true | :forbidden | nil
- :private | :maintainer | true | false | :unauthorized | nil
- :private | :guest | true | false | :unauthorized | nil
- :private | :maintainer | false | true | :not_found | nil
- :private | :guest | false | true | :not_found | nil
- :private | :maintainer | false | false | :unauthorized | nil
- :private | :guest | false | false | :unauthorized | nil
- :private | :anonymous | false | true | hidden_status | nil
+ where(:visibility_level, :user_type, :auth_method, :expected_status, :expected_body) do
+ :public | :developer | :basic | success_status | success_body
+ :public | :developer | :private_token | :unauthorized | nil
+ :public | :guest | :basic | :forbidden | nil
+ :public | :not_a_member | :basic | :forbidden | nil
+ :public | :anonymous | :basic | :unauthorized | nil
+ :public | :invalid_token | :basic | :unauthorized | nil
+ :private | :developer | :basic | success_status | success_body
+ :private | :guest | :basic | :forbidden | nil
+ :private | :not_a_member | :basic | :not_found | nil
+ :private | :anonymous | :basic | :unauthorized | nil
+ :private | :invalid_token | :basic | :unauthorized | nil
end
with_them do
- include_context 'Debian repository access', params[:visibility_level], params[:user_role], params[:member], params[:user_token], :basic do
- it_behaves_like "Debian repository #{desired_behavior}", params[:expected_status], params[:expected_body]
+ include_context 'Debian repository access', params[:visibility_level], params[:user_type], params[:auth_method] do
+ it_behaves_like "Debian packages #{desired_behavior} request", params[:expected_status], params[:expected_body]
end
end
end
- it_behaves_like 'rejects Debian access with unknown container id', hidden_status
+ it_behaves_like 'rejects Debian access with unknown container id', :unauthorized, :basic
end
diff --git a/spec/support/shared_examples/requests/api/graphql/mutations/destroy_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/mutations/destroy_list_shared_examples.rb
index 0cec67ff541..dca152223fb 100644
--- a/spec/support/shared_examples/requests/api/graphql/mutations/destroy_list_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/mutations/destroy_list_shared_examples.rb
@@ -28,7 +28,7 @@ RSpec.shared_examples 'board lists destroy request' do
it 'returns an error' do
subject
- expect(graphql_errors.first['message']).to include("The resource that you are attempting to access does not exist or you don't have permission to perform this action")
+ expect(graphql_errors.first['message']).to include(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
end
end
diff --git a/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb
index 41a61ba5fd7..d576a5874fd 100644
--- a/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb
@@ -2,12 +2,26 @@
RSpec.shared_examples 'a package detail' do
it_behaves_like 'a working graphql query' do
- it 'matches the JSON schema' do
- expect(package_details).to match_schema('graphql/packages/package_details')
+ it_behaves_like 'matching the package details schema'
+ end
+
+ context 'with pipelines' do
+ let_it_be(:build_info1) { create(:package_build_info, :with_pipeline, package: package) }
+ let_it_be(:build_info2) { create(:package_build_info, :with_pipeline, package: package) }
+ let_it_be(:build_info3) { create(:package_build_info, :with_pipeline, package: package) }
+
+ it_behaves_like 'a working graphql query' do
+ it_behaves_like 'matching the package details schema'
end
end
end
+RSpec.shared_examples 'matching the package details schema' do
+ it 'matches the JSON schema' do
+ expect(package_details).to match_schema('graphql/packages/package_details')
+ end
+end
+
RSpec.shared_examples 'a package with files' do
it 'has the right amount of files' do
expect(package_files_response.length).to be(package.package_files.length)
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 40799688144..0434d0beb7e 100644
--- a/spec/support/shared_examples/requests/api/notes_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/notes_shared_examples.rb
@@ -281,7 +281,7 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
end
end
- context 'when request exceeds the rate limit' do
+ context 'when request exceeds the rate limit', :freeze_time, :clean_gitlab_redis_rate_limiting do
before do
stub_application_setting(notes_create_limit: 1)
allow(::Gitlab::ApplicationRateLimiter).to receive(:increment).and_return(2)
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 2af7b616659..19677e92001 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
@@ -8,6 +8,8 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project|
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_it_be(:package_metadatum) { create(:npm_metadatum, package: package) }
+
let(:headers) { {} }
subject { get(url, headers: headers) }
@@ -39,6 +41,19 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project|
# query count can slightly change between the examples so we're using a custom threshold
expect { get(url, headers: headers) }.not_to exceed_query_limit(control).with_threshold(4)
end
+
+ context 'with packages_npm_abbreviated_metadata disabled' do
+ before do
+ stub_feature_flags(packages_npm_abbreviated_metadata: false)
+ end
+
+ it 'calls the presenter without including metadata' do
+ expect(::Packages::Npm::PackagePresenter)
+ .to receive(:new).with(anything, anything, include_metadata: false).and_call_original
+
+ subject
+ end
+ end
end
shared_examples 'reject metadata request' do |status:|
diff --git a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb
index ed6d9ed43c8..06c51add438 100644
--- a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb
@@ -167,7 +167,7 @@ end
RSpec.shared_examples 'rejects PyPI access with unknown project id' do
context 'with an unknown project' do
- let(:project) { OpenStruct.new(id: 1234567890) }
+ let(:project) { double('access', id: 1234567890) }
it_behaves_like 'unknown PyPI scope id'
end
@@ -175,7 +175,7 @@ end
RSpec.shared_examples 'rejects PyPI access with unknown group id' do
context 'with an unknown project' do
- let(:group) { OpenStruct.new(id: 1234567890) }
+ let(:group) { double('access', id: 1234567890) }
it_behaves_like 'unknown PyPI scope id'
end
diff --git a/spec/support/shared_examples/requests/api/status_shared_examples.rb b/spec/support/shared_examples/requests/api/status_shared_examples.rb
index 8207190b1dc..40843ccbd15 100644
--- a/spec/support/shared_examples/requests/api/status_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/status_shared_examples.rb
@@ -76,3 +76,32 @@ RSpec.shared_examples '412 response' do
end
end
end
+
+RSpec.shared_examples '422 response' do
+ let(:message) { nil }
+
+ before do
+ # Fires the request
+ request
+ end
+
+ it 'returns 422' do
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ expect(json_response).to be_an Object
+
+ if message.present?
+ expect(json_response['message']).to eq(message)
+ end
+ end
+end
+
+RSpec.shared_examples '503 response' do
+ before do
+ # Fires the request
+ request
+ end
+
+ it 'returns 503' do
+ expect(response).to have_gitlab_http_status(:service_unavailable)
+ end
+end
diff --git a/spec/support/shared_examples/requests/applications_controller_shared_examples.rb b/spec/support/shared_examples/requests/applications_controller_shared_examples.rb
new file mode 100644
index 00000000000..8f852d42c2c
--- /dev/null
+++ b/spec/support/shared_examples/requests/applications_controller_shared_examples.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'applications controller - GET #show' do
+ describe 'GET #show' do
+ it 'renders template' do
+ get show_path
+
+ expect(response).to render_template :show
+ end
+
+ context 'when application is viewed after being created' do
+ before do
+ create_application
+ end
+
+ it 'sets `@created` instance variable to `true`' do
+ get show_path
+
+ expect(assigns[:created]).to eq(true)
+ end
+ end
+
+ context 'when application is reviewed' do
+ it 'sets `@created` instance variable to `false`' do
+ get show_path
+
+ expect(assigns[:created]).to eq(false)
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'applications controller - POST #create' do
+ it "sets `#{OauthApplications::CREATED_SESSION_KEY}` session key to `true`" do
+ create_application
+
+ expect(session[OauthApplications::CREATED_SESSION_KEY]).to eq(true)
+ end
+end
+
+def create_application
+ create_params = attributes_for(:application, trusted: true, confidential: false, scopes: ['api'])
+ post create_path, params: { doorkeeper_application: create_params }
+end
diff --git a/spec/support/shared_examples/requests/self_monitoring_shared_examples.rb b/spec/support/shared_examples/requests/self_monitoring_shared_examples.rb
index ff87fc5d8df..f8a752a5673 100644
--- a/spec/support/shared_examples/requests/self_monitoring_shared_examples.rb
+++ b/spec/support/shared_examples/requests/self_monitoring_shared_examples.rb
@@ -39,6 +39,10 @@ end
# let(:status_api) { status_create_self_monitoring_project_admin_application_settings_path }
# subject { post create_self_monitoring_project_admin_application_settings_path }
RSpec.shared_examples 'triggers async worker, returns sidekiq job_id with response accepted' do
+ before do
+ allow(worker_class).to receive(:with_status).and_return(worker_class)
+ end
+
it 'returns sidekiq job_id of expected length' do
subject
diff --git a/spec/support/shared_examples/requests/snippet_shared_examples.rb b/spec/support/shared_examples/requests/snippet_shared_examples.rb
index dae3a3e74be..b13c4da0bed 100644
--- a/spec/support/shared_examples/requests/snippet_shared_examples.rb
+++ b/spec/support/shared_examples/requests/snippet_shared_examples.rb
@@ -86,6 +86,7 @@ RSpec.shared_examples 'snippet blob content' do
expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq 'true'
expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
+ expect(response.parsed_body).to be_empty
end
context 'when snippet repository is empty' do
diff --git a/spec/support/shared_examples/service_desk_issue_templates_examples.rb b/spec/support/shared_examples/service_desk_issue_templates_examples.rb
index fd9645df7a3..ed6c5199936 100644
--- a/spec/support/shared_examples/service_desk_issue_templates_examples.rb
+++ b/spec/support/shared_examples/service_desk_issue_templates_examples.rb
@@ -3,10 +3,10 @@
RSpec.shared_examples 'issue description templates from current project only' do
it 'loads issue description templates from the project only' do
within('#service-desk-template-select') do
- expect(page).to have_content('project-issue-bar')
- expect(page).to have_content('project-issue-foo')
- expect(page).not_to have_content('group-issue-bar')
- expect(page).not_to have_content('group-issue-foo')
+ expect(page).to have_content(:all, 'project-issue-bar')
+ expect(page).to have_content(:all, 'project-issue-foo')
+ expect(page).not_to have_content(:all, 'group-issue-bar')
+ expect(page).not_to have_content(:all, 'group-issue-foo')
end
end
end
diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb
index 92a7d7ab3a3..ca86cb082a7 100644
--- a/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb
+++ b/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb
@@ -3,7 +3,10 @@
# This shared_example requires the following variables:
# - `service`, the service which includes AlertManagement::AlertProcessing
RSpec.shared_examples 'creates an alert management alert or errors' do
- it { is_expected.to be_success }
+ specify do
+ expect(subject).to be_success
+ expect(subject.payload).to match(alerts: all(a_kind_of(AlertManagement::Alert)))
+ end
it 'creates AlertManagement::Alert' do
expect(Gitlab::AppLogger).not_to receive(:warn)
@@ -89,6 +92,7 @@ RSpec.shared_examples 'adds an alert management alert event' do
expect { subject }.to change { alert.reload.events }.by(1)
expect(subject).to be_success
+ expect(subject.payload).to match(alerts: all(a_kind_of(AlertManagement::Alert)))
end
it_behaves_like 'does not create an alert management alert'
diff --git a/spec/support/shared_examples/services/jira/requests/base_shared_examples.rb b/spec/support/shared_examples/services/jira/requests/base_shared_examples.rb
index 56a6d24d557..c4f6273b46c 100644
--- a/spec/support/shared_examples/services/jira/requests/base_shared_examples.rb
+++ b/spec/support/shared_examples/services/jira/requests/base_shared_examples.rb
@@ -26,11 +26,14 @@ RSpec.shared_examples 'a service that handles Jira API errors' do
expect(subject).to be_a(ServiceResponse)
expect(subject).to be_error
- expect(subject.message).to include(expected_message)
+ expect(subject.message).to start_with(expected_message)
end
end
context 'when the JSON in JIRA::HTTPError is unsafe' do
+ config_docs_link_url = Rails.application.routes.url_helpers.help_page_path('integration/jira/configure')
+ let(:docs_link_start) { '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: config_docs_link_url } }
+
before do
stub_client_and_raise(JIRA::HTTPError, error)
end
@@ -39,7 +42,8 @@ RSpec.shared_examples 'a service that handles Jira API errors' do
let(:error) { '{"errorMessages":' }
it 'returns the default error message' do
- expect(subject.message).to eq('An error occurred while requesting data from Jira. Check your Jira integration configuration and try again.')
+ error_message = 'An error occurred while requesting data from Jira. Check your %{docs_link_start}Jira integration configuration</a> and try again.' % { docs_link_start: docs_link_start }
+ expect(subject.message).to eq(error_message)
end
end
@@ -47,7 +51,8 @@ RSpec.shared_examples 'a service that handles Jira API errors' do
let(:error) { '{"errorMessages":["<script>alert(true)</script>foo"]}' }
it 'sanitizes it' do
- expect(subject.message).to eq('An error occurred while requesting data from Jira: foo. Check your Jira integration configuration and try again.')
+ error_message = 'An error occurred while requesting data from Jira: foo. Check your %{docs_link_start}Jira integration configuration</a> and try again.' % { docs_link_start: docs_link_start }
+ expect(subject.message).to eq(error_message)
end
end
end
diff --git a/spec/support/shared_examples/services/resource_events/synthetic_notes_builder_shared_examples.rb b/spec/support/shared_examples/services/resource_events/synthetic_notes_builder_shared_examples.rb
new file mode 100644
index 00000000000..716bee39fca
--- /dev/null
+++ b/spec/support/shared_examples/services/resource_events/synthetic_notes_builder_shared_examples.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'filters by paginated notes' do |event_type|
+ let(:event) { create(event_type) } # rubocop:disable Rails/SaveBang
+
+ before do
+ create(event_type, issue: event.issue)
+ end
+
+ it 'only returns given notes' do
+ paginated_notes = { event_type.to_s.pluralize => [double(id: event.id)] }
+ notes = described_class.new(event.issue, user, paginated_notes: paginated_notes).execute
+
+ expect(notes.size).to eq(1)
+ expect(notes.first.event).to eq(event)
+ end
+
+ context 'when paginated notes is empty' do
+ it 'does not return any notes' do
+ notes = described_class.new(event.issue, user, paginated_notes: {}).execute
+
+ expect(notes.size).to eq(0)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/workers/self_monitoring_shared_examples.rb b/spec/support/shared_examples/workers/self_monitoring_shared_examples.rb
index 89c0841fbd6..e6da96e12ec 100644
--- a/spec/support/shared_examples/workers/self_monitoring_shared_examples.rb
+++ b/spec/support/shared_examples/workers/self_monitoring_shared_examples.rb
@@ -17,7 +17,7 @@ end
RSpec.shared_examples 'returns in_progress based on Sidekiq::Status' do
it 'returns true when job is enqueued' do
- jid = described_class.perform_async
+ jid = described_class.with_status.perform_async
expect(described_class.in_progress?(jid)).to eq(true)
end
diff --git a/spec/support/stub_snowplow.rb b/spec/support/stub_snowplow.rb
index a21ce2399d7..c6e3b40972f 100644
--- a/spec/support/stub_snowplow.rb
+++ b/spec/support/stub_snowplow.rb
@@ -8,8 +8,6 @@ module StubSnowplow
host = 'localhost'
# rubocop:disable RSpec/AnyInstanceOf
- allow_any_instance_of(Gitlab::Tracking::Destinations::ProductAnalytics).to receive(:event)
-
allow_any_instance_of(Gitlab::Tracking::Destinations::Snowplow)
.to receive(:emitter)
.and_return(SnowplowTracker::Emitter.new(host, buffer_size: buffer_size))
diff --git a/spec/support/test_reports/test_reports_helper.rb b/spec/support/test_reports/test_reports_helper.rb
index 18b40a20cf1..85483062958 100644
--- a/spec/support/test_reports/test_reports_helper.rb
+++ b/spec/support/test_reports/test_reports_helper.rb
@@ -95,9 +95,9 @@ module TestReportsHelper
<<-EOF.strip_heredoc
junit.framework.AssertionFailedError: expected:&lt;1&gt; but was:&lt;3&gt;
at CalculatorTest.subtractExpression(Unknown Source)
- at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
- at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
- at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
+ at java.base/jdk.internal.database.NativeMethodAccessorImpl.invoke0(Native Method)
+ at java.base/jdk.internal.database.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
+ at java.base/jdk.internal.database.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
EOF
end
end
diff --git a/spec/support/time_travel.rb b/spec/support/time_travel.rb
new file mode 100644
index 00000000000..9dfbfd20524
--- /dev/null
+++ b/spec/support/time_travel.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'active_support/testing/time_helpers'
+
+RSpec.configure do |config|
+ config.include ActiveSupport::Testing::TimeHelpers
+
+ config.around(:example, :freeze_time) do |example|
+ freeze_time { example.run }
+ end
+
+ config.around(:example, :time_travel_to) do |example|
+ date_or_time = example.metadata[:time_travel_to]
+
+ unless date_or_time.respond_to?(:to_time) && date_or_time.to_time.present?
+ raise 'The time_travel_to RSpec metadata must have a Date or Time value.'
+ end
+
+ travel_to(date_or_time) { example.run }
+ end
+end
diff --git a/spec/support_specs/database/multiple_databases_spec.rb b/spec/support_specs/database/multiple_databases_spec.rb
index 6ad15fd6594..10d1a8277c6 100644
--- a/spec/support_specs/database/multiple_databases_spec.rb
+++ b/spec/support_specs/database/multiple_databases_spec.rb
@@ -19,19 +19,19 @@ RSpec.describe 'Database::MultipleDatabases' do
end
end
- context 'on Ci::CiDatabaseRecord' do
+ context 'on Ci::ApplicationRecord' do
before do
skip_if_multiple_databases_not_setup
end
it 'raises exception' do
- expect { Ci::CiDatabaseRecord.establish_connection(:ci) }.to raise_error /Cannot re-establish/
+ expect { Ci::ApplicationRecord.establish_connection(:ci) }.to raise_error /Cannot re-establish/
end
context 'when using with_reestablished_active_record_base' do
it 'does not raise exception' do
with_reestablished_active_record_base do
- expect { Ci::CiDatabaseRecord.establish_connection(:main) }.not_to raise_error
+ expect { Ci::ApplicationRecord.establish_connection(:main) }.not_to raise_error
end
end
end
diff --git a/spec/support_specs/helpers/stub_feature_flags_spec.rb b/spec/support_specs/helpers/stub_feature_flags_spec.rb
index 8629e895fd1..9b35fe35259 100644
--- a/spec/support_specs/helpers/stub_feature_flags_spec.rb
+++ b/spec/support_specs/helpers/stub_feature_flags_spec.rb
@@ -97,7 +97,7 @@ RSpec.describe StubFeatureFlags do
context 'type handling' do
context 'raises error' do
where(:feature_actors) do
- ['string', 1, 1.0, OpenStruct.new]
+ ['string', 1, 1.0, Object.new]
end
with_them do
diff --git a/spec/support_specs/time_travel_spec.rb b/spec/support_specs/time_travel_spec.rb
new file mode 100644
index 00000000000..8fa51c0c1f0
--- /dev/null
+++ b/spec/support_specs/time_travel_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'time travel' do
+ describe ':freeze_time' do
+ it 'freezes time around a spec example', :freeze_time do
+ expect { sleep 0.1 }.not_to change { Time.now.to_f }
+ end
+ end
+
+ describe ':time_travel_to' do
+ it 'time-travels to the specified date', time_travel_to: '2020-01-01' do
+ expect(Date.current).to eq(Date.new(2020, 1, 1))
+ end
+
+ it 'time-travels to the specified date & time', time_travel_to: '2020-02-02 10:30:45 -0700' do
+ expect(Time.current).to eq(Time.new(2020, 2, 2, 17, 30, 45, '+00:00'))
+ end
+ end
+end
diff --git a/spec/tasks/gitlab/db_rake_spec.rb b/spec/tasks/gitlab/db_rake_spec.rb
index ad4ada9a9f1..38392f77307 100644
--- a/spec/tasks/gitlab/db_rake_spec.rb
+++ b/spec/tasks/gitlab/db_rake_spec.rb
@@ -201,9 +201,11 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout do
describe 'reindex' do
let(:reindex) { double('reindex') }
let(:indexes) { double('indexes') }
+ let(:databases) { Gitlab::Database.database_base_models }
+ let(:databases_count) { databases.count }
it 'cleans up any leftover indexes' do
- expect(Gitlab::Database::Reindexing).to receive(:cleanup_leftovers!)
+ expect(Gitlab::Database::Reindexing).to receive(:cleanup_leftovers!).exactly(databases_count).times
run_rake_task('gitlab:db:reindex')
end
@@ -212,8 +214,8 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout do
it 'executes async index creation prior to any reindexing actions' do
stub_feature_flags(database_async_index_creation: true)
- expect(Gitlab::Database::AsyncIndexes).to receive(:create_pending_indexes!).ordered
- expect(Gitlab::Database::Reindexing).to receive(:perform).ordered
+ expect(Gitlab::Database::AsyncIndexes).to receive(:create_pending_indexes!).ordered.exactly(databases_count).times
+ expect(Gitlab::Database::Reindexing).to receive(:automatic_reindexing).ordered.exactly(databases_count).times
run_rake_task('gitlab:db:reindex')
end
@@ -229,38 +231,30 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout do
end
end
- context 'when no index_name is given' do
+ context 'calls automatic reindexing' do
it 'uses all candidate indexes' do
- expect(Gitlab::Database::PostgresIndex).to receive(:reindexing_support).and_return(indexes)
- expect(Gitlab::Database::Reindexing).to receive(:perform).with(indexes)
+ expect(Gitlab::Database::Reindexing).to receive(:automatic_reindexing).exactly(databases_count).times
run_rake_task('gitlab:db:reindex')
end
end
+ end
- context 'with index name given' do
- let(:index) { double('index') }
-
- before do
- allow(Gitlab::Database::PostgresIndex).to receive(:reindexing_support).and_return(indexes)
- end
-
- it 'calls the index rebuilder with the proper arguments' do
- allow(indexes).to receive(:where).with(identifier: 'public.foo_idx').and_return([index])
- expect(Gitlab::Database::Reindexing).to receive(:perform).with([index])
-
- run_rake_task('gitlab:db:reindex', '[public.foo_idx]')
- end
+ describe 'enqueue_reindexing_action' do
+ let(:index_name) { 'public.users_pkey' }
- it 'raises an error if the index does not exist' do
- allow(indexes).to receive(:where).with(identifier: 'public.absent_index').and_return([])
+ it 'creates an entry in the queue' do
+ expect do
+ run_rake_task('gitlab:db:enqueue_reindexing_action', "[#{index_name}, main]")
+ end.to change { Gitlab::Database::PostgresIndex.find(index_name).queued_reindexing_actions.size }.from(0).to(1)
+ end
- expect { run_rake_task('gitlab:db:reindex', '[public.absent_index]') }.to raise_error(/Index not found/)
- end
+ it 'defaults to main database' do
+ expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(ActiveRecord::Base.connection).and_call_original
- it 'raises an error if the index is not fully qualified with a schema' do
- expect { run_rake_task('gitlab:db:reindex', '[foo_idx]') }.to raise_error(/Index name is not fully qualified/)
- end
+ expect do
+ run_rake_task('gitlab:db:enqueue_reindexing_action', "[#{index_name}]")
+ end.to change { Gitlab::Database::PostgresIndex.find(index_name).queued_reindexing_actions.size }.from(0).to(1)
end
end
diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb
index 5adea832995..c5625db922d 100644
--- a/spec/tasks/gitlab/gitaly_rake_spec.rb
+++ b/spec/tasks/gitlab/gitaly_rake_spec.rb
@@ -67,34 +67,57 @@ RSpec.describe 'gitlab:gitaly namespace rake task', :silence_stdout do
end
it 'calls gmake in the gitaly directory' do
- expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['/usr/bin/gmake', 0])
- expect(Gitlab::Popen).to receive(:popen).with(%w[gmake], nil, { "BUNDLE_GEMFILE" => nil, "RUBYOPT" => nil }).and_return(true)
+ expect(Gitlab::Popen).to receive(:popen)
+ .with(%w[which gmake])
+ .and_return(['/usr/bin/gmake', 0])
+ expect(Gitlab::Popen).to receive(:popen)
+ .with(%w[gmake all git], nil, { "BUNDLE_GEMFILE" => nil, "RUBYOPT" => nil })
+ .and_return(['ok', 0])
subject
end
+
+ context 'when gmake fails' do
+ it 'aborts process' do
+ expect(Gitlab::Popen).to receive(:popen)
+ .with(%w[which gmake])
+ .and_return(['/usr/bin/gmake', 0])
+ expect(Gitlab::Popen).to receive(:popen)
+ .with(%w[gmake all git], nil, { "BUNDLE_GEMFILE" => nil, "RUBYOPT" => nil })
+ .and_return(['output', 1])
+
+ expect { subject }.to raise_error /Gitaly failed to compile: output/
+ end
+ end
end
context 'gmake is not available' do
before do
expect(main_object).to receive(:checkout_or_clone_version)
- expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['', 42])
+ expect(Gitlab::Popen).to receive(:popen)
+ .with(%w[which gmake])
+ .and_return(['', 42])
end
it 'calls make in the gitaly directory' do
- expect(Gitlab::Popen).to receive(:popen).with(%w[make], nil, { "BUNDLE_GEMFILE" => nil, "RUBYOPT" => nil }).and_return(true)
+ expect(Gitlab::Popen).to receive(:popen)
+ .with(%w[make all git], nil, { "BUNDLE_GEMFILE" => nil, "RUBYOPT" => nil })
+ .and_return(['output', 0])
subject
end
context 'when Rails.env is test' do
- let(:command) { %w[make] }
+ let(:command) { %w[make all git] }
before do
stub_rails_env('test')
end
it 'calls make in the gitaly directory with BUNDLE_DEPLOYMENT and GEM_HOME variables' do
- expect(Gitlab::Popen).to receive(:popen).with(command, nil, { "BUNDLE_GEMFILE" => nil, "RUBYOPT" => nil, "BUNDLE_DEPLOYMENT" => 'false', "GEM_HOME" => Bundler.bundle_path.to_s }).and_return(true)
+ expect(Gitlab::Popen).to receive(:popen)
+ .with(command, nil, { "BUNDLE_GEMFILE" => nil, "RUBYOPT" => nil, "BUNDLE_DEPLOYMENT" => 'false', "GEM_HOME" => Bundler.bundle_path.to_s })
+ .and_return(['/usr/bin/gmake', 0])
subject
end
diff --git a/spec/tasks/gitlab/storage_rake_spec.rb b/spec/tasks/gitlab/storage_rake_spec.rb
index 570f67c8bb7..38a031178ae 100644
--- a/spec/tasks/gitlab/storage_rake_spec.rb
+++ b/spec/tasks/gitlab/storage_rake_spec.rb
@@ -90,7 +90,7 @@ RSpec.describe 'rake gitlab:storage:*', :silence_stdout do
shared_examples 'wait until database is ready' do
it 'checks if the database is ready once' do
- expect(Gitlab::Database.main).to receive(:exists?).once
+ expect(ApplicationRecord.database).to receive(:exists?).once
run_rake_task(task)
end
@@ -102,7 +102,7 @@ RSpec.describe 'rake gitlab:storage:*', :silence_stdout do
end
it 'tries for 3 times, polling every 0.1 seconds' do
- expect(Gitlab::Database.main).to receive(:exists?).exactly(3).times.and_return(false)
+ expect(ApplicationRecord.database).to receive(:exists?).exactly(3).times.and_return(false)
run_rake_task(task)
end
diff --git a/spec/tooling/danger/changelog_spec.rb b/spec/tooling/danger/changelog_spec.rb
index 5777186cc28..377c3e881c9 100644
--- a/spec/tooling/danger/changelog_spec.rb
+++ b/spec/tooling/danger/changelog_spec.rb
@@ -228,7 +228,7 @@ RSpec.describe Tooling::Danger::Changelog do
end
context 'with changelog label' do
- let(:mr_labels) { ['feature'] }
+ let(:mr_labels) { ['type::feature'] }
it 'is truthy' do
is_expected.to be_truthy
@@ -236,7 +236,7 @@ RSpec.describe Tooling::Danger::Changelog do
end
context 'with no changelog label' do
- let(:mr_labels) { ['tooling'] }
+ let(:mr_labels) { ['type::tooling'] }
it 'is truthy' do
is_expected.to be_falsey
diff --git a/spec/tooling/danger/product_intelligence_spec.rb b/spec/tooling/danger/product_intelligence_spec.rb
index 5fd44ef5de0..c090dbb4de4 100644
--- a/spec/tooling/danger/product_intelligence_spec.rb
+++ b/spec/tooling/danger/product_intelligence_spec.rb
@@ -44,20 +44,26 @@ RSpec.describe Tooling::Danger::ProductIntelligence do
context 'with product intelligence label' do
let(:expected_labels) { ['product intelligence::review pending'] }
+ let(:mr_labels) { [] }
before do
allow(fake_helper).to receive(:mr_has_labels?).with('product intelligence').and_return(true)
+ allow(fake_helper).to receive(:mr_labels).and_return(mr_labels)
end
it { is_expected.to match_array(expected_labels) }
- end
- context 'with product intelligence::review pending' do
- before do
- allow(fake_helper).to receive(:mr_has_labels?).and_return(true)
+ context 'with product intelligence::review pending' do
+ let(:mr_labels) { ['product intelligence::review pending'] }
+
+ it { is_expected.to be_empty }
end
- it { is_expected.to be_empty }
+ context 'with product intelligence::approved' do
+ let(:mr_labels) { ['product intelligence::approved'] }
+
+ it { is_expected.to be_empty }
+ end
end
context 'with growth experiment label' do
@@ -68,71 +74,4 @@ RSpec.describe Tooling::Danger::ProductIntelligence do
it { is_expected.to be_empty }
end
end
-
- describe '#matching_changed_files' do
- subject { product_intelligence.matching_changed_files }
-
- let(:changed_files) do
- [
- 'dashboard/todos_controller.rb',
- 'components/welcome.vue',
- 'admin/groups/_form.html.haml'
- ]
- end
-
- context 'with snowplow files changed' do
- context 'when vue file changed' do
- let(:changed_lines) { ['+data-track-action'] }
-
- it { is_expected.to match_array(['components/welcome.vue']) }
- end
-
- context 'when haml file changed' do
- let(:changed_lines) { ['+ data: { track_label:'] }
-
- it { is_expected.to match_array(['admin/groups/_form.html.haml']) }
- end
-
- context 'when ruby file changed' do
- let(:changed_lines) { ['+ Gitlab::Tracking.event'] }
- let(:changed_files) { ['dashboard/todos_controller.rb', 'admin/groups/_form.html.haml'] }
-
- it { is_expected.to match_array(['dashboard/todos_controller.rb']) }
- end
- end
-
- context 'with metrics files changed' do
- let(:changed_files) { ['config/metrics/counts_7d/test_metric.yml', 'ee/config/metrics/counts_7d/ee_metric.yml'] }
-
- it { is_expected.to match_array(changed_files) }
- end
-
- context 'with metrics files not changed' do
- it { is_expected.to be_empty }
- end
-
- context 'with tracking files changed' do
- let(:changed_files) do
- [
- 'lib/gitlab/tracking.rb',
- 'spec/lib/gitlab/tracking_spec.rb',
- 'app/helpers/tracking_helper.rb'
- ]
- end
-
- it { is_expected.to match_array(changed_files) }
- end
-
- context 'with usage_data files changed' do
- let(:changed_files) do
- [
- 'doc/api/usage_data.md',
- 'ee/lib/ee/gitlab/usage_data.rb',
- 'spec/lib/gitlab/usage_data_spec.rb'
- ]
- end
-
- it { is_expected.to match_array(changed_files) }
- end
- end
end
diff --git a/spec/tooling/danger/project_helper_spec.rb b/spec/tooling/danger/project_helper_spec.rb
index 5edd9e54cc5..ec475df6d83 100644
--- a/spec/tooling/danger/project_helper_spec.rb
+++ b/spec/tooling/danger/project_helper_spec.rb
@@ -7,8 +7,10 @@ require 'danger/plugins/helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../danger/plugins/project_helper'
+require_relative '../../../spec/support/helpers/stub_env'
RSpec.describe Tooling::Danger::ProjectHelper do
+ include StubENV
include_context "with dangerfile"
let(:fake_danger) { DangerSpecHelper.fake_danger.include(described_class) }
@@ -40,7 +42,7 @@ RSpec.describe Tooling::Danger::ProjectHelper do
using RSpec::Parameterized::TableSyntax
before do
- allow(fake_git).to receive(:diff_for_file).with('usage_data.rb') { double(:diff, patch: "+ count(User.active)") }
+ allow(fake_git).to receive(:diff_for_file).with(instance_of(String)) { double(:diff, patch: "+ count(User.active)") }
end
where(:path, :expected_categories) do
@@ -189,6 +191,58 @@ RSpec.describe Tooling::Danger::ProjectHelper do
'spec/frontend/tracking/foo.js' | [:frontend, :product_intelligence]
'spec/frontend/tracking_spec.js' | [:frontend, :product_intelligence]
'lib/gitlab/usage_database/foo.rb' | [:backend]
+ 'config/metrics/counts_7d/test_metric.yml' | [:product_intelligence]
+ 'config/metrics/schema.json' | [:product_intelligence]
+ 'doc/api/usage_data.md' | [:product_intelligence]
+ 'spec/lib/gitlab/usage_data_spec.rb' | [:product_intelligence]
+
+ 'app/models/integration.rb' | [:integrations_be, :backend]
+ 'ee/app/models/integrations/github.rb' | [:integrations_be, :backend]
+ 'ee/app/models/ee/integrations/jira.rb' | [:integrations_be, :backend]
+ 'app/models/integrations/chat_message/pipeline_message.rb' | [:integrations_be, :backend]
+ 'app/models/jira_connect_subscription.rb' | [:integrations_be, :backend]
+ 'app/models/hooks/service_hook.rb' | [:integrations_be, :backend]
+ 'ee/app/models/ee/hooks/system_hook.rb' | [:integrations_be, :backend]
+ 'app/services/concerns/integrations/project_test_data.rb' | [:integrations_be, :backend]
+ 'ee/app/services/ee/integrations/test/project_service.rb' | [:integrations_be, :backend]
+ 'app/controllers/concerns/integrations/actions.rb' | [:integrations_be, :backend]
+ 'ee/app/controllers/concerns/ee/integrations/params.rb' | [:integrations_be, :backend]
+ 'ee/app/controllers/projects/integrations/jira/issues_controller.rb' | [:integrations_be, :backend]
+ 'app/controllers/projects/hooks_controller.rb' | [:integrations_be, :backend]
+ 'app/controllers/admin/hook_logs_controller.rb' | [:integrations_be, :backend]
+ 'app/controllers/groups/settings/integrations_controller.rb' | [:integrations_be, :backend]
+ 'app/controllers/jira_connect/branches_controller.rb' | [:integrations_be, :backend]
+ 'app/controllers/oauth/jira/authorizations_controller.rb' | [:integrations_be, :backend]
+ 'ee/app/finders/projects/integrations/jira/by_ids_finder.rb' | [:integrations_be, :database, :backend]
+ 'app/workers/jira_connect/sync_merge_request_worker.rb' | [:integrations_be, :backend]
+ 'app/workers/propagate_integration_inherit_worker.rb' | [:integrations_be, :backend]
+ 'app/workers/web_hooks/log_execution_worker.rb' | [:integrations_be, :backend]
+ 'app/workers/web_hook_worker.rb' | [:integrations_be, :backend]
+ 'app/workers/project_service_worker.rb' | [:integrations_be, :backend]
+ 'lib/atlassian/jira_connect/serializers/commit_entity.rb' | [:integrations_be, :backend]
+ 'lib/api/entities/project_integration.rb' | [:integrations_be, :backend]
+ 'lib/gitlab/hook_data/note_builder.rb' | [:integrations_be, :backend]
+ 'lib/gitlab/data_builder/note.rb' | [:integrations_be, :backend]
+ 'ee/lib/ee/gitlab/integrations/sti_type.rb' | [:integrations_be, :backend]
+ 'ee/lib/ee/api/helpers/integrations_helpers.rb' | [:integrations_be, :backend]
+ 'ee/app/serializers/integrations/jira_serializers/issue_entity.rb' | [:integrations_be, :backend]
+ 'lib/api/github/entities.rb' | [:integrations_be, :backend]
+ 'lib/api/v3/github.rb' | [:integrations_be, :backend]
+ 'app/models/clusters/integrations/elastic_stack.rb' | [:backend]
+ 'app/controllers/clusters/integrations_controller.rb' | [:backend]
+ 'app/services/clusters/integrations/prometheus_health_check_service.rb' | [:backend]
+ 'app/graphql/types/alert_management/integration_type.rb' | [:backend]
+
+ 'app/views/jira_connect/branches/new.html.haml' | [:integrations_fe, :frontend]
+ 'app/views/layouts/jira_connect.html.haml' | [:integrations_fe, :frontend]
+ 'app/assets/javascripts/jira_connect/branches/pages/index.vue' | [:integrations_fe, :frontend]
+ 'ee/app/views/projects/integrations/jira/issues/show.html.haml' | [:integrations_fe, :frontend]
+ 'ee/app/assets/javascripts/integrations/zentao/issues_list/graphql/queries/get_zentao_issues.query.graphql' | [:integrations_fe, :frontend]
+ 'app/assets/javascripts/pages/projects/settings/integrations/show/index.js' | [:integrations_fe, :frontend]
+ 'ee/app/assets/javascripts/pages/groups/hooks/index.js' | [:integrations_fe, :frontend]
+ 'app/views/clusters/clusters/_integrations_tab.html.haml' | [:frontend]
+ 'app/assets/javascripts/alerts_settings/graphql/fragments/integration_item.fragment.graphql' | [:frontend]
+ 'app/assets/javascripts/filtered_search/droplab/hook_input.js' | [:frontend]
end
with_them do
@@ -199,12 +253,20 @@ RSpec.describe Tooling::Danger::ProjectHelper do
context 'having specific changes' do
where(:expected_categories, :patch, :changed_files) do
+ [:product_intelligence] | '+data-track-action' | ['components/welcome.vue']
+ [:product_intelligence] | '+ data: { track_label:' | ['admin/groups/_form.html.haml']
+ [:product_intelligence] | '+ Gitlab::Tracking.event' | ['dashboard/todos_controller.rb', 'admin/groups/_form.html.haml']
[:database, :backend, :product_intelligence] | '+ count(User.active)' | ['usage_data.rb', 'lib/gitlab/usage_data.rb', 'ee/lib/ee/gitlab/usage_data.rb']
[:database, :backend, :product_intelligence] | '+ estimate_batch_distinct_count(User.active)' | ['usage_data.rb']
[:backend, :product_intelligence] | '+ alt_usage_data(User.active)' | ['lib/gitlab/usage_data.rb']
[:backend, :product_intelligence] | '+ count(User.active)' | ['lib/gitlab/usage_data/topology.rb']
[:backend, :product_intelligence] | '+ foo_count(User.active)' | ['lib/gitlab/usage_data.rb']
[:backend] | '+ count(User.active)' | ['user.rb']
+ [:integrations_be, :database, :migration] | '+ add_column :integrations, :foo, :text' | ['db/migrate/foo.rb']
+ [:integrations_be, :database, :migration] | '+ create_table :zentao_tracker_data do |t|' | ['ee/db/post_migrate/foo.rb']
+ [:integrations_be, :backend] | '+ Integrations::Foo' | ['app/foo/bar.rb']
+ [:integrations_be, :backend] | '+ project.execute_hooks(foo, :bar)' | ['ee/lib/ee/foo.rb']
+ [:integrations_be, :backend] | '+ project.execute_integrations(foo, :bar)' | ['app/foo.rb']
end
with_them do
@@ -281,6 +343,70 @@ RSpec.describe Tooling::Danger::ProjectHelper do
end
end
+ describe '#ee?' do
+ subject { project_helper.__send__(:ee?) }
+
+ let(:ee_dir) { File.expand_path('../../../ee', __dir__) }
+
+ context 'when ENV["CI_PROJECT_NAME"] is set' do
+ before do
+ stub_env('CI_PROJECT_NAME', ci_project_name)
+ end
+
+ context 'when ENV["CI_PROJECT_NAME"] is gitlab' do
+ let(:ci_project_name) { 'gitlab' }
+
+ it 'returns true' do
+ is_expected.to eq(true)
+ end
+ end
+
+ context 'when ENV["CI_PROJECT_NAME"] is gitlab-ee' do
+ let(:ci_project_name) { 'gitlab-ee' }
+
+ it 'returns true' do
+ is_expected.to eq(true)
+ end
+ end
+
+ context 'when ENV["CI_PROJECT_NAME"] is gitlab-foss' do
+ let(:ci_project_name) { 'gitlab-foss' }
+
+ it 'resolves to Dir.exist?' do
+ expected = Dir.exist?(ee_dir)
+
+ expect(Dir).to receive(:exist?).with(ee_dir).and_call_original
+
+ is_expected.to eq(expected)
+ end
+ end
+ end
+
+ context 'when ENV["CI_PROJECT_NAME"] is absent' do
+ before do
+ stub_env('CI_PROJECT_NAME', nil)
+
+ expect(Dir).to receive(:exist?).with(ee_dir).and_return(has_ee_dir)
+ end
+
+ context 'when ee/ directory exists' do
+ let(:has_ee_dir) { true }
+
+ it 'returns true' do
+ is_expected.to eq(true)
+ end
+ end
+
+ context 'when ee/ directory does not exist' do
+ let(:has_ee_dir) { false }
+
+ it 'returns false' do
+ is_expected.to eq(false)
+ end
+ end
+ end
+ end
+
describe '#file_lines' do
let(:filename) { 'spec/foo_spec.rb' }
let(:file_spy) { spy }
diff --git a/spec/tooling/quality/test_level_spec.rb b/spec/tooling/quality/test_level_spec.rb
index 0623a67a60e..94fa9d682e1 100644
--- a/spec/tooling/quality/test_level_spec.rb
+++ b/spec/tooling/quality/test_level_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Quality::TestLevel do
context 'when level is unit' do
it 'returns a pattern' do
expect(subject.pattern(:unit))
- .to eq("spec/{bin,channels,config,db,dependencies,elastic,elastic_integration,experiments,factories,finders,frontend,graphql,haml_lint,helpers,initializers,javascripts,lib,models,policies,presenters,rack_servers,replicators,routing,rubocop,serializers,services,sidekiq,spam,support_specs,tasks,uploaders,validators,views,workers,tooling}{,/**/}*_spec.rb")
+ .to eq("spec/{bin,channels,config,db,dependencies,elastic,elastic_integration,experiments,factories,finders,frontend,graphql,haml_lint,helpers,initializers,javascripts,lib,models,policies,presenters,rack_servers,replicators,routing,rubocop,scripts,serializers,services,sidekiq,spam,support_specs,tasks,uploaders,validators,views,workers,tooling}{,/**/}*_spec.rb")
end
end
@@ -49,7 +49,7 @@ RSpec.describe Quality::TestLevel do
context 'when level is integration' do
it 'returns a pattern' do
expect(subject.pattern(:integration))
- .to eq("spec/{controllers,mailers,requests}{,/**/}*_spec.rb")
+ .to eq("spec/{commands,controllers,mailers,requests}{,/**/}*_spec.rb")
end
end
@@ -110,7 +110,7 @@ RSpec.describe Quality::TestLevel do
context 'when level is unit' do
it 'returns a regexp' do
expect(subject.regexp(:unit))
- .to eq(%r{spec/(bin|channels|config|db|dependencies|elastic|elastic_integration|experiments|factories|finders|frontend|graphql|haml_lint|helpers|initializers|javascripts|lib|models|policies|presenters|rack_servers|replicators|routing|rubocop|serializers|services|sidekiq|spam|support_specs|tasks|uploaders|validators|views|workers|tooling)})
+ .to eq(%r{spec/(bin|channels|config|db|dependencies|elastic|elastic_integration|experiments|factories|finders|frontend|graphql|haml_lint|helpers|initializers|javascripts|lib|models|policies|presenters|rack_servers|replicators|routing|rubocop|scripts|serializers|services|sidekiq|spam|support_specs|tasks|uploaders|validators|views|workers|tooling)})
end
end
@@ -131,7 +131,7 @@ RSpec.describe Quality::TestLevel do
context 'when level is integration' do
it 'returns a regexp' do
expect(subject.regexp(:integration))
- .to eq(%r{spec/(controllers|mailers|requests)})
+ .to eq(%r{spec/(commands|controllers|mailers|requests)})
end
end
@@ -204,6 +204,10 @@ RSpec.describe Quality::TestLevel do
expect(subject.level_for('spec/mailers/abuse_report_mailer_spec.rb')).to eq(:integration)
end
+ it 'returns the correct level for an integration test in a subfolder' do
+ expect(subject.level_for('spec/commands/sidekiq_cluster/cli.rb')).to eq(:integration)
+ end
+
it 'returns the correct level for a system test' do
expect(subject.level_for('spec/features/abuse_report_spec.rb')).to eq(:system)
end
diff --git a/spec/validators/addressable_url_validator_spec.rb b/spec/validators/addressable_url_validator_spec.rb
index ec3ee9aa500..7e2cc2afa8a 100644
--- a/spec/validators/addressable_url_validator_spec.rb
+++ b/spec/validators/addressable_url_validator_spec.rb
@@ -14,6 +14,7 @@ RSpec.describe AddressableUrlValidator do
describe 'validations' do
include_context 'invalid urls'
+ include_context 'valid urls with CRLF'
let(:validator) { described_class.new(attributes: [:link_url]) }
@@ -27,9 +28,20 @@ RSpec.describe AddressableUrlValidator do
expect(badge.errors.added?(:link_url, validator.options.fetch(:message))).to be true
end
+ it 'allows urls with encoded CR or LF characters' do
+ aggregate_failures do
+ valid_urls_with_CRLF.each do |url|
+ validator.validate_each(badge, :link_url, url)
+
+ expect(badge.errors).to be_empty
+ end
+ end
+ end
+
it 'does not allow urls with CR or LF characters' do
aggregate_failures do
urls_with_CRLF.each do |url|
+ badge = build(:badge, link_url: 'http://www.example.com')
validator.validate_each(badge, :link_url, url)
expect(badge.errors.added?(:link_url, 'is blocked: URI is invalid')).to be true
diff --git a/spec/views/groups/settings/_remove.html.haml_spec.rb b/spec/views/groups/settings/_remove.html.haml_spec.rb
index 07fe900bc2d..e40fda58a72 100644
--- a/spec/views/groups/settings/_remove.html.haml_spec.rb
+++ b/spec/views/groups/settings/_remove.html.haml_spec.rb
@@ -9,8 +9,8 @@ RSpec.describe 'groups/settings/_remove.html.haml' do
render 'groups/settings/remove', group: group
- expect(rendered).to have_selector '[data-testid="remove-group-button"]'
- expect(rendered).not_to have_selector '[data-testid="remove-group-button"].disabled'
+ expect(rendered).to have_selector '[data-button-testid="remove-group-button"]'
+ expect(rendered).not_to have_selector '[data-button-testid="remove-group-button"].disabled'
expect(rendered).not_to have_selector '[data-testid="group-has-linked-subscription-alert"]'
end
end
diff --git a/spec/views/groups/settings/_transfer.html.haml_spec.rb b/spec/views/groups/settings/_transfer.html.haml_spec.rb
index b557c989eae..911eb5b7ab3 100644
--- a/spec/views/groups/settings/_transfer.html.haml_spec.rb
+++ b/spec/views/groups/settings/_transfer.html.haml_spec.rb
@@ -9,9 +9,9 @@ RSpec.describe 'groups/settings/_transfer.html.haml' do
render 'groups/settings/transfer', group: group
- expect(rendered).to have_selector '[data-qa-selector="select_group_dropdown"]' # rubocop:disable QA/SelectorUsage
- expect(rendered).not_to have_selector '[data-qa-selector="select_group_dropdown"][disabled]' # rubocop:disable QA/SelectorUsage
- expect(rendered).not_to have_selector '[data-testid="group-to-transfer-has-linked-subscription-alert"]'
+ expect(rendered).to have_button 'Select parent group'
+ expect(rendered).not_to have_button 'Select parent group', disabled: true
+ expect(rendered).not_to have_text "This group can't be transfered because it is linked to a subscription."
end
end
end
diff --git a/spec/views/jira_connect/subscriptions/index.html.haml_spec.rb b/spec/views/jira_connect/subscriptions/index.html.haml_spec.rb
index dcc36c93327..0a4d283a983 100644
--- a/spec/views/jira_connect/subscriptions/index.html.haml_spec.rb
+++ b/spec/views/jira_connect/subscriptions/index.html.haml_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe 'jira_connect/subscriptions/index.html.haml' do
before do
allow(view).to receive(:current_user).and_return(user)
- assign(:subscriptions, [])
+ assign(:subscriptions, create_list(:jira_connect_subscription, 1))
end
context 'when the user is signed in' do
diff --git a/spec/views/layouts/_published_experiments.html.haml_spec.rb b/spec/views/layouts/_published_experiments.html.haml_spec.rb
new file mode 100644
index 00000000000..d1ade8ddd6e
--- /dev/null
+++ b/spec/views/layouts/_published_experiments.html.haml_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'layouts/_published_experiments', :experiment do
+ before do
+ stub_const('TestControlExperiment', ApplicationExperiment)
+ stub_const('TestCandidateExperiment', ApplicationExperiment)
+ stub_const('TestExcludedExperiment', ApplicationExperiment)
+
+ TestControlExperiment.new('test_control').tap do |e|
+ e.variant(:control)
+ e.publish
+ end
+ TestCandidateExperiment.new('test_candidate').tap do |e|
+ e.variant(:candidate)
+ e.publish
+ end
+ TestExcludedExperiment.new('test_excluded').tap do |e|
+ e.exclude!
+ e.publish
+ end
+
+ render
+ end
+
+ it 'renders out data for all non-excluded, published experiments' do
+ output = rendered
+
+ expect(output).to include('gl.experiments = {')
+ expect(output).to match(/"test_control":\{[^}]*"variant":"control"/)
+ expect(output).to match(/"test_candidate":\{[^}]*"variant":"candidate"/)
+ expect(output).not_to include('"test_excluded"')
+ 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 20c5d9992be..f7da288b9f3 100644
--- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
@@ -987,28 +987,10 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
end
describe 'Usage Quotas' do
- context 'with project_storage_ui feature flag enabled' do
- before do
- stub_feature_flags(project_storage_ui: true)
- end
-
- it 'has a link to Usage Quotas' do
- render
-
- expect(rendered).to have_link('Usage Quotas', href: project_usage_quotas_path(project))
- end
- end
-
- context 'with project_storage_ui feature flag disabled' do
- before do
- stub_feature_flags(project_storage_ui: false)
- end
-
- it 'does not have a link to Usage Quotas' do
- render
+ it 'has a link to Usage Quotas' do
+ render
- expect(rendered).not_to have_link('Usage Quotas', href: project_usage_quotas_path(project))
- end
+ expect(rendered).to have_link('Usage Quotas', href: project_usage_quotas_path(project))
end
end
end
diff --git a/spec/views/profiles/audit_log.html.haml_spec.rb b/spec/views/profiles/audit_log.html.haml_spec.rb
new file mode 100644
index 00000000000..d5f6a2d64e7
--- /dev/null
+++ b/spec/views/profiles/audit_log.html.haml_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'profiles/audit_log' do
+ let(:user) { create(:user) }
+
+ before do
+ assign(:user, user)
+ assign(:events, AuthenticationEvent.all.page(params[:page]))
+ allow(controller).to receive(:current_user).and_return(user)
+ end
+
+ context 'when user has successful and failure events' do
+ before do
+ create(:authentication_event, :successful, user: user)
+ create(:authentication_event, :failed, user: user)
+ end
+
+ it 'only shows successful events' do
+ render
+
+ expect(rendered).to have_text('Signed in with standard authentication', count: 1)
+ end
+ end
+end
diff --git a/spec/views/projects/edit.html.haml_spec.rb b/spec/views/projects/edit.html.haml_spec.rb
index b44d07d2ee4..60f4c1664f7 100644
--- a/spec/views/projects/edit.html.haml_spec.rb
+++ b/spec/views/projects/edit.html.haml_spec.rb
@@ -57,6 +57,41 @@ RSpec.describe 'projects/edit' do
end
end
+ context 'merge commit template' do
+ it 'displays all possible variables' do
+ render
+
+ expect(rendered).to have_content('%{source_branch}')
+ expect(rendered).to have_content('%{target_branch}')
+ expect(rendered).to have_content('%{title}')
+ expect(rendered).to have_content('%{issues}')
+ expect(rendered).to have_content('%{description}')
+ expect(rendered).to have_content('%{reference}')
+ end
+
+ it 'displays a placeholder if none is set' do
+ render
+
+ expect(rendered).to have_field('project[merge_commit_template]', placeholder: <<~MSG.rstrip)
+ Merge branch '%{source_branch}' into '%{target_branch}'
+
+ %{title}
+
+ %{issues}
+
+ See merge request %{reference}
+ MSG
+ end
+
+ it 'displays the user entered value' do
+ project.update!(merge_commit_template: '%{title}')
+
+ render
+
+ expect(rendered).to have_field('project[merge_commit_template]', with: '%{title}')
+ end
+ end
+
context 'forking' do
before do
assign(:project, project)
diff --git a/spec/views/projects/issues/_service_desk_info_content.html.haml_spec.rb b/spec/views/projects/issues/_service_desk_info_content.html.haml_spec.rb
new file mode 100644
index 00000000000..1c6d729ddce
--- /dev/null
+++ b/spec/views/projects/issues/_service_desk_info_content.html.haml_spec.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'projects/issues/_service_desk_info_content' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:service_desk_address) { 'address@example.com' }
+
+ before do
+ assign(:project, project)
+ allow(project).to receive(:service_desk_address).and_return(service_desk_address)
+ allow(view).to receive(:current_user).and_return(user)
+ end
+
+ context 'when service desk is disabled' do
+ before do
+ allow(project).to receive(:service_desk_enabled?).and_return(false)
+ end
+
+ context 'when the logged user is at least maintainer' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'shows the info including the project settings link', :aggregate_failures do
+ render
+
+ expect(rendered).to have_text('Use Service Desk')
+ expect(rendered).not_to have_text(service_desk_address)
+ expect(rendered).to have_link(href: "/#{project.full_path}/edit")
+ end
+ end
+
+ context 'when the logged user is at only a developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'shows the info without the project settings link', :aggregate_failures do
+ render
+
+ expect(rendered).to have_text('Use Service Desk')
+ expect(rendered).not_to have_text(service_desk_address)
+ expect(rendered).not_to have_link(href: "/#{project.full_path}/edit")
+ end
+ end
+ end
+
+ context 'when service desk is enabled' do
+ before do
+ allow(project).to receive(:service_desk_enabled?).and_return(true)
+ end
+
+ context 'when the logged user is at least reporter' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it 'shows the info including the email address', :aggregate_failures do
+ render
+
+ expect(rendered).to have_text('Use Service Desk')
+ expect(rendered).to have_text(service_desk_address)
+ expect(rendered).not_to have_link(href: "/#{project.full_path}/edit")
+ end
+ end
+
+ context 'when the logged user is at only a guest' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'shows the info without the email address', :aggregate_failures do
+ render
+
+ expect(rendered).to have_text('Use Service Desk')
+ expect(rendered).not_to have_text(service_desk_address)
+ expect(rendered).not_to have_link(href: "/#{project.full_path}/edit")
+ end
+ end
+
+ context 'when user is not logged in' do
+ let(:user) { nil }
+
+ it 'shows the info without the email address', :aggregate_failures do
+ render
+
+ expect(rendered).to have_text('Use Service Desk')
+ expect(rendered).not_to have_text(service_desk_address)
+ expect(rendered).not_to have_link(href: "/#{project.full_path}/edit")
+ end
+ end
+ end
+end
diff --git a/spec/workers/analytics/usage_trends/counter_job_worker_spec.rb b/spec/workers/analytics/usage_trends/counter_job_worker_spec.rb
index dd180229d12..c45ec20fe5a 100644
--- a/spec/workers/analytics/usage_trends/counter_job_worker_spec.rb
+++ b/spec/workers/analytics/usage_trends/counter_job_worker_spec.rb
@@ -11,7 +11,8 @@ RSpec.describe Analytics::UsageTrends::CounterJobWorker do
let(:job_args) { [users_measurement_identifier, user_1.id, user_2.id, recorded_at] }
before do
- allow(::Analytics::UsageTrends::Measurement.connection).to receive(:transaction_open?).and_return(false)
+ allow(::ApplicationRecord.connection).to receive(:transaction_open?).and_return(false)
+ allow(::Ci::ApplicationRecord.connection).to receive(:transaction_open?).and_return(false) if ::Ci::ApplicationRecord.connection_class?
end
include_examples 'an idempotent worker' do
diff --git a/spec/workers/ci/ref_delete_unlock_artifacts_worker_spec.rb b/spec/workers/ci/ref_delete_unlock_artifacts_worker_spec.rb
index f510852e753..fe4bc2421a4 100644
--- a/spec/workers/ci/ref_delete_unlock_artifacts_worker_spec.rb
+++ b/spec/workers/ci/ref_delete_unlock_artifacts_worker_spec.rb
@@ -4,7 +4,9 @@ require 'spec_helper'
RSpec.describe Ci::RefDeleteUnlockArtifactsWorker do
describe '#perform' do
- subject(:perform) { described_class.new.perform(project_id, user_id, ref) }
+ subject(:perform) { worker.perform(project_id, user_id, ref) }
+
+ let(:worker) { described_class.new }
let(:ref) { 'refs/heads/master' }
@@ -40,6 +42,36 @@ RSpec.describe Ci::RefDeleteUnlockArtifactsWorker do
expect(service).to have_received(:execute).with(ci_ref)
end
+
+ context 'when a locked pipeline with persisted artifacts exists' do
+ let!(:pipeline) { create(:ci_pipeline, :with_persisted_artifacts, ref: 'master', project: project, locked: :artifacts_locked) }
+
+ context 'with ci_update_unlocked_job_artifacts disabled' do
+ before do
+ stub_feature_flags(ci_update_unlocked_job_artifacts: false)
+ end
+
+ it 'logs the correct extra metadata' do
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:unlocked_pipelines, 1)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:unlocked_job_artifacts, 0)
+
+ perform
+ end
+ end
+
+ context 'with ci_update_unlocked_job_artifacts enabled' do
+ before do
+ stub_feature_flags(ci_update_unlocked_job_artifacts: true)
+ end
+
+ it 'logs the correct extra metadata' do
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:unlocked_pipelines, 1)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:unlocked_job_artifacts, 2)
+
+ perform
+ end
+ end
+ end
end
context 'when ci ref does not exist for the given project' do
diff --git a/spec/workers/ci/resource_groups/assign_resource_from_resource_group_worker_spec.rb b/spec/workers/ci/resource_groups/assign_resource_from_resource_group_worker_spec.rb
index 650be1e84a9..be7f7ef5c8c 100644
--- a/spec/workers/ci/resource_groups/assign_resource_from_resource_group_worker_spec.rb
+++ b/spec/workers/ci/resource_groups/assign_resource_from_resource_group_worker_spec.rb
@@ -9,6 +9,10 @@ RSpec.describe Ci::ResourceGroups::AssignResourceFromResourceGroupWorker do
expect(described_class.get_deduplicate_strategy).to eq(:until_executed)
end
+ it 'has an option to reschedule once if deduplicated' do
+ expect(described_class.get_deduplication_options).to include({ if_deduplicated: :reschedule_once })
+ end
+
describe '#perform' do
subject { worker.perform(resource_group_id) }
diff --git a/spec/workers/clusters/applications/check_prometheus_health_worker_spec.rb b/spec/workers/clusters/integrations/check_prometheus_health_worker_spec.rb
index fb779bf3b01..6f70870bd09 100644
--- a/spec/workers/clusters/applications/check_prometheus_health_worker_spec.rb
+++ b/spec/workers/clusters/integrations/check_prometheus_health_worker_spec.rb
@@ -2,16 +2,16 @@
require 'spec_helper'
-RSpec.describe Clusters::Applications::CheckPrometheusHealthWorker, '#perform' do
+RSpec.describe Clusters::Integrations::CheckPrometheusHealthWorker, '#perform' do
subject { described_class.new.perform }
it 'triggers health service' do
cluster = create(:cluster)
allow(Gitlab::Monitor::DemoProjects).to receive(:primary_keys)
- allow(Clusters::Cluster).to receive_message_chain(:with_application_prometheus, :with_project_http_integrations).and_return([cluster])
+ allow(Clusters::Cluster).to receive_message_chain(:with_integration_prometheus, :with_project_http_integrations).and_return([cluster])
- service_instance = instance_double(Clusters::Applications::PrometheusHealthCheckService)
- expect(Clusters::Applications::PrometheusHealthCheckService).to receive(:new).with(cluster).and_return(service_instance)
+ service_instance = instance_double(Clusters::Integrations::PrometheusHealthCheckService)
+ expect(Clusters::Integrations::PrometheusHealthCheckService).to receive(:new).with(cluster).and_return(service_instance)
expect(service_instance).to receive(:execute)
subject
diff --git a/spec/workers/concerns/application_worker_spec.rb b/spec/workers/concerns/application_worker_spec.rb
index af038c81b9e..fbf39b3c7cd 100644
--- a/spec/workers/concerns/application_worker_spec.rb
+++ b/spec/workers/concerns/application_worker_spec.rb
@@ -285,48 +285,38 @@ RSpec.describe ApplicationWorker do
end
end
- describe '.bulk_perform_async' do
- before do
- stub_const(worker.name, worker)
+ context 'different kinds of push_bulk' do
+ shared_context 'disable the `sidekiq_push_bulk_in_batches` feature flag' do
+ before do
+ stub_feature_flags(sidekiq_push_bulk_in_batches: false)
+ end
end
- it 'enqueues jobs in bulk' do
- Sidekiq::Testing.fake! do
- worker.bulk_perform_async([['Foo', [1]], ['Foo', [2]]])
-
- expect(worker.jobs.count).to eq 2
- expect(worker.jobs).to all(include('enqueued_at'))
+ shared_context 'set safe limit beyond the number of jobs to be enqueued' do
+ before do
+ stub_const("#{described_class}::SAFE_PUSH_BULK_LIMIT", args.count + 1)
end
end
- end
- describe '.bulk_perform_in' do
- before do
- stub_const(worker.name, worker)
+ shared_context 'set safe limit below the number of jobs to be enqueued' do
+ before do
+ stub_const("#{described_class}::SAFE_PUSH_BULK_LIMIT", 2)
+ end
end
- context 'when delay is valid' do
- it 'correctly schedules jobs' do
- Sidekiq::Testing.fake! do
- worker.bulk_perform_in(1.minute, [['Foo', [1]], ['Foo', [2]]])
+ shared_examples_for 'returns job_id of all enqueued jobs' do
+ let(:job_id_regex) { /[0-9a-f]{12}/ }
- expect(worker.jobs.count).to eq 2
- expect(worker.jobs).to all(include('at'))
- end
- end
- end
+ it 'returns job_id of all enqueued jobs' do
+ job_ids = perform_action
- context 'when delay is invalid' do
- it 'raises an ArgumentError exception' do
- expect { worker.bulk_perform_in(-60, [['Foo']]) }
- .to raise_error(ArgumentError)
+ expect(job_ids.count).to eq(args.count)
+ expect(job_ids).to all(match(job_id_regex))
end
end
- context 'with batches' do
- let(:batch_delay) { 1.minute }
-
- it 'correctly schedules jobs' do
+ shared_examples_for 'enqueues the jobs in a batched fashion, with each batch enqueing jobs as per the set safe limit' do
+ it 'enqueues the jobs in a batched fashion, with each batch enqueing jobs as per the set safe limit' do
expect(Sidekiq::Client).to(
receive(:push_bulk).with(hash_including('args' => [['Foo', [1]], ['Foo', [2]]]))
.ordered
@@ -337,29 +327,318 @@ RSpec.describe ApplicationWorker do
.and_call_original)
expect(Sidekiq::Client).to(
receive(:push_bulk).with(hash_including('args' => [['Foo', [5]]]))
- .ordered
- .and_call_original)
+ .ordered
+ .and_call_original)
- worker.bulk_perform_in(
- 1.minute,
- [['Foo', [1]], ['Foo', [2]], ['Foo', [3]], ['Foo', [4]], ['Foo', [5]]],
- batch_size: 2, batch_delay: batch_delay)
-
- expect(worker.jobs.count).to eq 5
- expect(worker.jobs[0]['at']).to eq(worker.jobs[1]['at'])
- expect(worker.jobs[2]['at']).to eq(worker.jobs[3]['at'])
- expect(worker.jobs[2]['at'] - worker.jobs[1]['at']).to eq(batch_delay)
- expect(worker.jobs[4]['at'] - worker.jobs[3]['at']).to eq(batch_delay)
- end
-
- context 'when batch_size is invalid' do
- it 'raises an ArgumentError exception' do
- expect do
- worker.bulk_perform_in(1.minute,
- [['Foo']],
- batch_size: -1, batch_delay: batch_delay)
- end.to raise_error(ArgumentError)
+ perform_action
+
+ expect(worker.jobs.count).to eq args.count
+ expect(worker.jobs).to all(include('enqueued_at'))
+ end
+ end
+
+ shared_examples_for 'enqueues jobs in one go' do
+ it 'enqueues jobs in one go' do
+ expect(Sidekiq::Client).to(
+ receive(:push_bulk).with(hash_including('args' => args)).once.and_call_original)
+ expect(Sidekiq.logger).not_to receive(:info)
+
+ perform_action
+
+ expect(worker.jobs.count).to eq args.count
+ expect(worker.jobs).to all(include('enqueued_at'))
+ end
+ end
+
+ shared_examples_for 'logs bulk insertions' do
+ it 'logs arguments and job IDs' do
+ worker.log_bulk_perform_async!
+
+ expect(Sidekiq.logger).to(
+ receive(:info).with(hash_including('class' => worker.name, 'args_list' => args)).once.and_call_original)
+ expect(Sidekiq.logger).to(
+ receive(:info).with(hash_including('class' => worker.name, 'jid_list' => anything)).once.and_call_original)
+
+ perform_action
+ end
+ end
+
+ before do
+ stub_const(worker.name, worker)
+ end
+
+ let(:args) do
+ [
+ ['Foo', [1]],
+ ['Foo', [2]],
+ ['Foo', [3]],
+ ['Foo', [4]],
+ ['Foo', [5]]
+ ]
+ end
+
+ describe '.bulk_perform_async' do
+ shared_examples_for 'does not schedule the jobs for any specific time' do
+ it 'does not schedule the jobs for any specific time' do
+ perform_action
+
+ expect(worker.jobs).to all(exclude('at'))
+ end
+ end
+
+ subject(:perform_action) do
+ worker.bulk_perform_async(args)
+ end
+
+ context 'push_bulk in safe limit batches' do
+ context 'when the number of jobs to be enqueued does not exceed the safe limit' do
+ include_context 'set safe limit beyond the number of jobs to be enqueued'
+
+ it_behaves_like 'enqueues jobs in one go'
+ it_behaves_like 'logs bulk insertions'
+ it_behaves_like 'returns job_id of all enqueued jobs'
+ it_behaves_like 'does not schedule the jobs for any specific time'
end
+
+ context 'when the number of jobs to be enqueued exceeds safe limit' do
+ include_context 'set safe limit below the number of jobs to be enqueued'
+
+ it_behaves_like 'enqueues the jobs in a batched fashion, with each batch enqueing jobs as per the set safe limit'
+ it_behaves_like 'returns job_id of all enqueued jobs'
+ it_behaves_like 'does not schedule the jobs for any specific time'
+ end
+
+ context 'when the feature flag `sidekiq_push_bulk_in_batches` is disabled' do
+ include_context 'disable the `sidekiq_push_bulk_in_batches` feature flag'
+
+ context 'when the number of jobs to be enqueued does not exceed the safe limit' do
+ include_context 'set safe limit beyond the number of jobs to be enqueued'
+
+ it_behaves_like 'enqueues jobs in one go'
+ it_behaves_like 'logs bulk insertions'
+ it_behaves_like 'returns job_id of all enqueued jobs'
+ it_behaves_like 'does not schedule the jobs for any specific time'
+ end
+
+ context 'when the number of jobs to be enqueued exceeds safe limit' do
+ include_context 'set safe limit below the number of jobs to be enqueued'
+
+ it_behaves_like 'enqueues jobs in one go'
+ it_behaves_like 'returns job_id of all enqueued jobs'
+ it_behaves_like 'does not schedule the jobs for any specific time'
+ end
+ end
+ end
+ end
+
+ describe '.bulk_perform_in' do
+ context 'without batches' do
+ shared_examples_for 'schedules all the jobs at a specific time' do
+ it 'schedules all the jobs at a specific time' do
+ perform_action
+
+ worker.jobs.each do |job_detail|
+ expect(job_detail['at']).to be_within(3.seconds).of(expected_scheduled_at_time)
+ end
+ end
+ end
+
+ let(:delay) { 3.minutes }
+ let(:expected_scheduled_at_time) { Time.current.to_i + delay.to_i }
+
+ subject(:perform_action) do
+ worker.bulk_perform_in(delay, args)
+ end
+
+ context 'when the scheduled time falls in the past' do
+ let(:delay) { -60 }
+
+ it 'raises an ArgumentError exception' do
+ expect { perform_action }
+ .to raise_error(ArgumentError)
+ end
+ end
+
+ context 'push_bulk in safe limit batches' do
+ context 'when the number of jobs to be enqueued does not exceed the safe limit' do
+ include_context 'set safe limit beyond the number of jobs to be enqueued'
+
+ it_behaves_like 'enqueues jobs in one go'
+ it_behaves_like 'returns job_id of all enqueued jobs'
+ it_behaves_like 'schedules all the jobs at a specific time'
+ end
+
+ context 'when the number of jobs to be enqueued exceeds safe limit' do
+ include_context 'set safe limit below the number of jobs to be enqueued'
+
+ it_behaves_like 'enqueues the jobs in a batched fashion, with each batch enqueing jobs as per the set safe limit'
+ it_behaves_like 'returns job_id of all enqueued jobs'
+ it_behaves_like 'schedules all the jobs at a specific time'
+ end
+
+ context 'when the feature flag `sidekiq_push_bulk_in_batches` is disabled' do
+ include_context 'disable the `sidekiq_push_bulk_in_batches` feature flag'
+
+ context 'when the number of jobs to be enqueued does not exceed the safe limit' do
+ include_context 'set safe limit beyond the number of jobs to be enqueued'
+
+ it_behaves_like 'enqueues jobs in one go'
+ it_behaves_like 'returns job_id of all enqueued jobs'
+ it_behaves_like 'schedules all the jobs at a specific time'
+ end
+
+ context 'when the number of jobs to be enqueued exceeds safe limit' do
+ include_context 'set safe limit below the number of jobs to be enqueued'
+
+ it_behaves_like 'enqueues jobs in one go'
+ it_behaves_like 'returns job_id of all enqueued jobs'
+ it_behaves_like 'schedules all the jobs at a specific time'
+ end
+ end
+ end
+ end
+
+ context 'with batches' do
+ shared_examples_for 'schedules all the jobs at a specific time, per batch' do
+ it 'schedules all the jobs at a specific time, per batch' do
+ perform_action
+
+ expect(worker.jobs[0]['at']).to eq(worker.jobs[1]['at'])
+ expect(worker.jobs[2]['at']).to eq(worker.jobs[3]['at'])
+ expect(worker.jobs[2]['at'] - worker.jobs[1]['at']).to eq(batch_delay)
+ expect(worker.jobs[4]['at'] - worker.jobs[3]['at']).to eq(batch_delay)
+ end
+ end
+
+ let(:delay) { 1.minute }
+ let(:batch_size) { 2 }
+ let(:batch_delay) { 10.minutes }
+
+ subject(:perform_action) do
+ worker.bulk_perform_in(delay, args, batch_size: batch_size, batch_delay: batch_delay)
+ end
+
+ context 'when the `batch_size` is invalid' do
+ context 'when `batch_size` is 0' do
+ let(:batch_size) { 0 }
+
+ it 'raises an ArgumentError exception' do
+ expect { perform_action }
+ .to raise_error(ArgumentError)
+ end
+ end
+
+ context 'when `batch_size` is negative' do
+ let(:batch_size) { -3 }
+
+ it 'raises an ArgumentError exception' do
+ expect { perform_action }
+ .to raise_error(ArgumentError)
+ end
+ end
+ end
+
+ context 'when the `batch_delay` is invalid' do
+ context 'when `batch_delay` is 0' do
+ let(:batch_delay) { 0.minutes }
+
+ it 'raises an ArgumentError exception' do
+ expect { perform_action }
+ .to raise_error(ArgumentError)
+ end
+ end
+
+ context 'when `batch_delay` is negative' do
+ let(:batch_delay) { -3.minutes }
+
+ it 'raises an ArgumentError exception' do
+ expect { perform_action }
+ .to raise_error(ArgumentError)
+ end
+ end
+ end
+
+ context 'push_bulk in safe limit batches' do
+ context 'when the number of jobs to be enqueued does not exceed the safe limit' do
+ include_context 'set safe limit beyond the number of jobs to be enqueued'
+
+ it_behaves_like 'enqueues jobs in one go'
+ it_behaves_like 'returns job_id of all enqueued jobs'
+ it_behaves_like 'schedules all the jobs at a specific time, per batch'
+ end
+
+ context 'when the number of jobs to be enqueued exceeds safe limit' do
+ include_context 'set safe limit below the number of jobs to be enqueued'
+
+ it_behaves_like 'enqueues the jobs in a batched fashion, with each batch enqueing jobs as per the set safe limit'
+ it_behaves_like 'returns job_id of all enqueued jobs'
+ it_behaves_like 'schedules all the jobs at a specific time, per batch'
+ end
+
+ context 'when the feature flag `sidekiq_push_bulk_in_batches` is disabled' do
+ include_context 'disable the `sidekiq_push_bulk_in_batches` feature flag'
+
+ context 'when the number of jobs to be enqueued does not exceed the safe limit' do
+ include_context 'set safe limit beyond the number of jobs to be enqueued'
+
+ it_behaves_like 'enqueues jobs in one go'
+ it_behaves_like 'returns job_id of all enqueued jobs'
+ it_behaves_like 'schedules all the jobs at a specific time, per batch'
+ end
+
+ context 'when the number of jobs to be enqueued exceeds safe limit' do
+ include_context 'set safe limit below the number of jobs to be enqueued'
+
+ it_behaves_like 'enqueues jobs in one go'
+ it_behaves_like 'returns job_id of all enqueued jobs'
+ it_behaves_like 'schedules all the jobs at a specific time, per batch'
+ end
+ end
+ end
+ end
+ end
+ end
+
+ describe '.with_status' do
+ around do |example|
+ Sidekiq::Testing.fake!(&example)
+ end
+
+ context 'when the worker does have status_expiration set' do
+ let(:status_expiration_worker) do
+ Class.new(worker) do
+ sidekiq_options status_expiration: 3
+ end
+ end
+
+ it 'uses status_expiration from the worker' do
+ status_expiration_worker.with_status.perform_async
+
+ expect(Sidekiq::Queues[status_expiration_worker.queue].first).to include('status_expiration' => 3)
+ expect(Sidekiq::Queues[status_expiration_worker.queue].length).to eq(1)
+ end
+
+ it 'uses status_expiration from the worker without with_status' do
+ status_expiration_worker.perform_async
+
+ expect(Sidekiq::Queues[status_expiration_worker.queue].first).to include('status_expiration' => 3)
+ expect(Sidekiq::Queues[status_expiration_worker.queue].length).to eq(1)
+ end
+ end
+
+ context 'when the worker does not have status_expiration set' do
+ it 'uses the default status_expiration' do
+ worker.with_status.perform_async
+
+ expect(Sidekiq::Queues[worker.queue].first).to include('status_expiration' => Gitlab::SidekiqStatus::DEFAULT_EXPIRATION)
+ expect(Sidekiq::Queues[worker.queue].length).to eq(1)
+ end
+
+ it 'does not set status_expiration without with_status' do
+ worker.perform_async
+
+ expect(Sidekiq::Queues[worker.queue].first).not_to include('status_expiration')
+ expect(Sidekiq::Queues[worker.queue].length).to eq(1)
end
end
end
diff --git a/spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb b/spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb
index d4126fe688a..cbffb8f3870 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,8 +82,9 @@ RSpec.describe ContainerExpirationPolicies::CleanupContainerRepositoryWorker do
nil | 10 | nil
0 | 5 | nil
10 | 0 | 0
- 10 | 5 | 0.5
- 3 | 10 | (10 / 3.to_f)
+ 10 | 5 | 50.0
+ 17 | 3 | 17.65
+ 3 | 10 | 333.33
end
with_them do
diff --git a/spec/workers/database/drop_detached_partitions_worker_spec.rb b/spec/workers/database/drop_detached_partitions_worker_spec.rb
index 8693878ddd5..a10fcaaa5d9 100644
--- a/spec/workers/database/drop_detached_partitions_worker_spec.rb
+++ b/spec/workers/database/drop_detached_partitions_worker_spec.rb
@@ -6,21 +6,19 @@ RSpec.describe Database::DropDetachedPartitionsWorker do
describe '#perform' do
subject { described_class.new.perform }
- let(:monitoring) { instance_double('PartitionMonitoring', report_metrics: nil) }
-
before do
allow(Gitlab::Database::Partitioning).to receive(:drop_detached_partitions)
- allow(Gitlab::Database::Partitioning::PartitionMonitoring).to receive(:new).and_return(monitoring)
+ allow(Gitlab::Database::Partitioning).to receive(:report_metrics)
end
- it 'delegates to Partitioning.drop_detached_partitions' do
+ it 'drops detached partitions' do
expect(Gitlab::Database::Partitioning).to receive(:drop_detached_partitions)
subject
end
it 'reports partition metrics' do
- expect(monitoring).to receive(:report_metrics)
+ expect(Gitlab::Database::Partitioning).to receive(:report_metrics)
subject
end
diff --git a/spec/workers/database/partition_management_worker_spec.rb b/spec/workers/database/partition_management_worker_spec.rb
index 9ded36743a8..e5362e95f48 100644
--- a/spec/workers/database/partition_management_worker_spec.rb
+++ b/spec/workers/database/partition_management_worker_spec.rb
@@ -6,20 +6,19 @@ RSpec.describe Database::PartitionManagementWorker do
describe '#perform' do
subject { described_class.new.perform }
- let(:monitoring) { instance_double('PartitionMonitoring', report_metrics: nil) }
-
before do
- allow(Gitlab::Database::Partitioning::PartitionMonitoring).to receive(:new).and_return(monitoring)
+ allow(Gitlab::Database::Partitioning).to receive(:sync_partitions)
+ allow(Gitlab::Database::Partitioning).to receive(:report_metrics)
end
- it 'delegates to Partitioning' do
+ it 'syncs partitions' do
expect(Gitlab::Database::Partitioning).to receive(:sync_partitions)
subject
end
it 'reports partition metrics' do
- expect(monitoring).to receive(:report_metrics)
+ expect(Gitlab::Database::Partitioning).to receive(:report_metrics)
subject
end
diff --git a/spec/workers/dependency_proxy/image_ttl_group_policy_worker_spec.rb b/spec/workers/dependency_proxy/image_ttl_group_policy_worker_spec.rb
index d3234f4c212..ae0cb097ebf 100644
--- a/spec/workers/dependency_proxy/image_ttl_group_policy_worker_spec.rb
+++ b/spec/workers/dependency_proxy/image_ttl_group_policy_worker_spec.rb
@@ -12,8 +12,8 @@ RSpec.describe DependencyProxy::ImageTtlGroupPolicyWorker do
subject { worker.perform }
context 'when there are images to expire' do
- let_it_be_with_reload(:old_blob) { create(:dependency_proxy_blob, group: group, updated_at: 1.year.ago) }
- let_it_be_with_reload(:old_manifest) { create(:dependency_proxy_manifest, group: group, updated_at: 1.year.ago) }
+ let_it_be_with_reload(:old_blob) { create(:dependency_proxy_blob, group: group, read_at: 1.year.ago) }
+ let_it_be_with_reload(:old_manifest) { create(:dependency_proxy_manifest, group: group, read_at: 1.year.ago) }
let_it_be_with_reload(:new_blob) { create(:dependency_proxy_blob, group: group) }
let_it_be_with_reload(:new_manifest) { create(:dependency_proxy_manifest, group: group) }
diff --git a/spec/workers/deployments/archive_in_project_worker_spec.rb b/spec/workers/deployments/archive_in_project_worker_spec.rb
new file mode 100644
index 00000000000..6435fe8bea1
--- /dev/null
+++ b/spec/workers/deployments/archive_in_project_worker_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Deployments::ArchiveInProjectWorker do
+ subject { described_class.new.perform(deployment&.project_id) }
+
+ describe '#perform' do
+ let(:deployment) { create(:deployment, :success) }
+
+ it 'executes Deployments::ArchiveInProjectService' do
+ expect(Deployments::ArchiveInProjectService)
+ .to receive(:new).with(deployment.project, nil).and_call_original
+
+ subject
+ end
+ end
+end
diff --git a/spec/workers/email_receiver_worker_spec.rb b/spec/workers/email_receiver_worker_spec.rb
index 83e13ded7b3..83720ee132b 100644
--- a/spec/workers/email_receiver_worker_spec.rb
+++ b/spec/workers/email_receiver_worker_spec.rb
@@ -37,6 +37,15 @@ RSpec.describe EmailReceiverWorker, :mailer do
expect(email.to).to eq(["jake@adventuretime.ooo"])
expect(email.subject).to include("Rejected")
end
+
+ it 'strips out the body before passing to EmailRejectionMailer' do
+ mail = Mail.new(raw_message)
+ mail.body = nil
+
+ expect(EmailRejectionMailer).to receive(:rejection).with(anything, mail.encoded, anything).and_call_original
+
+ described_class.new.perform(raw_message)
+ end
end
context 'when the error is Gitlab::Email::AutoGeneratedEmailError' do
diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb
index 6c37c422aed..3e313610054 100644
--- a/spec/workers/emails_on_push_worker_spec.rb
+++ b/spec/workers/emails_on_push_worker_spec.rb
@@ -139,6 +139,43 @@ RSpec.describe EmailsOnPushWorker, :mailer do
perform
end
+
+ context 'when SMIME signing is enabled' do
+ include SmimeHelper
+
+ before :context do
+ @root_ca = generate_root
+ @cert = generate_cert(signer_ca: @root_ca)
+ end
+
+ let(:root_certificate) do
+ Gitlab::X509::Certificate.new(@root_ca[:key], @root_ca[:cert])
+ end
+
+ let(:certificate) do
+ Gitlab::X509::Certificate.new(@cert[:key], @cert[:cert])
+ end
+
+ before do
+ allow(Gitlab::Email::Hook::SmimeSignatureInterceptor).to receive(:certificate).and_return(certificate)
+
+ Mail.register_interceptor(Gitlab::Email::Hook::SmimeSignatureInterceptor)
+ end
+
+ after do
+ Mail.unregister_interceptor(Gitlab::Email::Hook::SmimeSignatureInterceptor)
+ end
+
+ it 'does not sign the email multiple times' do
+ perform
+
+ ActionMailer::Base.deliveries.each do |mail|
+ expect(mail.header['Content-Type'].value).to match('multipart/signed').and match('protocol="application/x-pkcs7-signature"')
+
+ expect(mail.to_s.scan(/Content-Disposition: attachment;\r\n filename=smime.p7s/).size).to eq(1)
+ end
+ end
+ end
end
context "when recipients are invalid" do
diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
index 9a4b27997e9..d00243672f9 100644
--- a/spec/workers/every_sidekiq_worker_spec.rb
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -316,6 +316,8 @@ RSpec.describe 'Every Sidekiq worker' do
'IssuableExportCsvWorker' => 3,
'IssuePlacementWorker' => 3,
'IssueRebalancingWorker' => 3,
+ 'Issues::PlacementWorker' => 3,
+ 'Issues::RebalancingWorker' => 3,
'IterationsUpdateStatusWorker' => 3,
'JiraConnect::SyncBranchWorker' => 3,
'JiraConnect::SyncBuildsWorker' => 3,
diff --git a/spec/workers/integrations/create_external_cross_reference_worker_spec.rb b/spec/workers/integrations/create_external_cross_reference_worker_spec.rb
new file mode 100644
index 00000000000..61723f44aa5
--- /dev/null
+++ b/spec/workers/integrations/create_external_cross_reference_worker_spec.rb
@@ -0,0 +1,128 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::CreateExternalCrossReferenceWorker do
+ include AfterNextHelpers
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:project) { create(:jira_project, :repository) }
+ let_it_be(:author) { create(:user) }
+ let_it_be(:commit) { project.commit }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let_it_be(:note) { create(:note, project: project) }
+ let_it_be(:snippet) { create(:project_snippet, project: project) }
+
+ let(:project_id) { project.id }
+ let(:external_issue_id) { 'JIRA-123' }
+ let(:mentionable_type) { 'Issue' }
+ let(:mentionable_id) { issue.id }
+ let(:author_id) { author.id }
+ let(:job_args) { [project_id, external_issue_id, mentionable_type, mentionable_id, author_id] }
+
+ def perform
+ described_class.new.perform(*job_args)
+ end
+
+ before do
+ allow(Project).to receive(:find_by_id).and_return(project)
+ end
+
+ it_behaves_like 'an idempotent worker' do
+ before do
+ allow(project.external_issue_tracker).to receive(:create_cross_reference_note)
+ end
+
+ it 'can run multiple times with the same arguments' do
+ subject
+
+ expect(project.external_issue_tracker).to have_received(:create_cross_reference_note)
+ .exactly(worker_exec_times).times
+ end
+ end
+
+ it 'has the `until_executed` deduplicate strategy' do
+ expect(described_class.get_deduplicate_strategy).to eq(:until_executed)
+ expect(described_class.get_deduplication_options).to include({ including_scheduled: true })
+ end
+
+ # These are the only models where we currently support cross-references,
+ # although this should be expanded to all `Mentionable` models.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/343975
+ where(:mentionable_type, :mentionable_id) do
+ 'Commit' | lazy { commit.id }
+ 'Issue' | lazy { issue.id }
+ 'MergeRequest' | lazy { merge_request.id }
+ 'Note' | lazy { note.id }
+ 'Snippet' | lazy { snippet.id }
+ end
+
+ with_them do
+ it 'creates a cross reference' do
+ expect(project.external_issue_tracker).to receive(:create_cross_reference_note).with(
+ be_a(ExternalIssue).and(have_attributes(id: external_issue_id, project: project)),
+ be_a(mentionable_type.constantize).and(have_attributes(id: mentionable_id)),
+ be_a(User).and(have_attributes(id: author_id))
+ )
+
+ perform
+ end
+ end
+
+ describe 'error handling' do
+ shared_examples 'does not create a cross reference' do
+ it 'does not create a cross reference' do
+ expect(project).not_to receive(:external_issue_tracker) if project
+
+ perform
+ end
+ end
+
+ context 'project_id does not exist' do
+ let(:project_id) { non_existing_record_id }
+ let(:project) { nil }
+
+ it_behaves_like 'does not create a cross reference'
+ end
+
+ context 'author_id does not exist' do
+ let(:author_id) { non_existing_record_id }
+
+ it_behaves_like 'does not create a cross reference'
+ end
+
+ context 'mentionable_id does not exist' do
+ let(:mentionable_id) { non_existing_record_id }
+
+ it_behaves_like 'does not create a cross reference'
+ end
+
+ context 'mentionable_type is not a Mentionable' do
+ let(:mentionable_type) { 'User' }
+
+ before do
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).with(kind_of(ArgumentError))
+ end
+
+ it_behaves_like 'does not create a cross reference'
+ end
+
+ context 'mentionable_type is not a defined constant' do
+ let(:mentionable_type) { 'FooBar' }
+
+ before do
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).with(kind_of(ArgumentError))
+ end
+
+ it_behaves_like 'does not create a cross reference'
+ end
+
+ context 'mentionable is a Commit and mentionable_id does not exist' do
+ let(:mentionable_type) { 'Commit' }
+ let(:mentionable_id) { non_existing_record_id }
+
+ it_behaves_like 'does not create a cross reference'
+ end
+ end
+end
diff --git a/spec/workers/issue_rebalancing_worker_spec.rb b/spec/workers/issue_rebalancing_worker_spec.rb
index cba42a1577e..cfb19af05b3 100644
--- a/spec/workers/issue_rebalancing_worker_spec.rb
+++ b/spec/workers/issue_rebalancing_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe IssueRebalancingWorker do
+RSpec.describe IssueRebalancingWorker, :clean_gitlab_redis_shared_state do
describe '#perform' do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
@@ -35,6 +35,20 @@ RSpec.describe IssueRebalancingWorker do
described_class.new.perform # all arguments are nil
end
+
+ it 'does not schedule a new rebalance if it finished under 1h ago' do
+ container_type = arguments.second.present? ? ::Gitlab::Issues::Rebalancing::State::PROJECT : ::Gitlab::Issues::Rebalancing::State::NAMESPACE
+ container_id = arguments.second || arguments.third
+
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set(::Gitlab::Issues::Rebalancing::State.send(:recently_finished_key, container_type, container_id), true)
+ end
+
+ expect(Issues::RelativePositionRebalancingService).not_to receive(:new)
+ expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
+
+ described_class.new.perform(*arguments)
+ end
end
shared_examples 'safely handles non-existent ids' do
diff --git a/spec/workers/issues/placement_worker_spec.rb b/spec/workers/issues/placement_worker_spec.rb
new file mode 100644
index 00000000000..694cdd2ef37
--- /dev/null
+++ b/spec/workers/issues/placement_worker_spec.rb
@@ -0,0 +1,151 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Issues::PlacementWorker do
+ describe '#perform' do
+ let_it_be(:time) { Time.now.utc }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:author) { create(:user) }
+ let_it_be(:common_attrs) { { author: author, project: project } }
+ let_it_be(:unplaced) { common_attrs.merge(relative_position: nil) }
+ let_it_be_with_reload(:issue) { create(:issue, **unplaced, created_at: time) }
+ let_it_be_with_reload(:issue_a) { create(:issue, **unplaced, created_at: time - 1.minute) }
+ let_it_be_with_reload(:issue_b) { create(:issue, **unplaced, created_at: time - 2.minutes) }
+ let_it_be_with_reload(:issue_c) { create(:issue, **unplaced, created_at: time + 1.minute) }
+ let_it_be_with_reload(:issue_d) { create(:issue, **unplaced, created_at: time + 2.minutes) }
+ let_it_be_with_reload(:issue_e) { create(:issue, **common_attrs, relative_position: 10, created_at: time + 1.minute) }
+ let_it_be_with_reload(:issue_f) { create(:issue, **unplaced, created_at: time + 1.minute) }
+
+ let_it_be(:irrelevant) { create(:issue, relative_position: nil, created_at: time) }
+
+ shared_examples 'running the issue placement worker' do
+ let(:issue_id) { issue.id }
+ let(:project_id) { project.id }
+
+ it 'places all issues created at most 5 minutes before this one at the end, most recent last' do
+ expect { run_worker }.not_to change { irrelevant.reset.relative_position }
+
+ expect(project.issues.order_by_relative_position)
+ .to eq([issue_e, issue_b, issue_a, issue, issue_c, issue_f, issue_d])
+ expect(project.issues.where(relative_position: nil)).not_to exist
+ end
+
+ it 'schedules rebalancing if needed' do
+ issue_a.update!(relative_position: RelativePositioning::MAX_POSITION)
+
+ expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, nil, project.group.id)
+
+ run_worker
+ end
+
+ context 'there are more than QUERY_LIMIT unplaced issues' do
+ before_all do
+ # Ensure there are more than N issues in this set
+ n = described_class::QUERY_LIMIT
+ create_list(:issue, n - 5, **unplaced)
+ end
+
+ it 'limits the sweep to QUERY_LIMIT records, and reschedules placement' do
+ expect(Issue).to receive(:move_nulls_to_end)
+ .with(have_attributes(count: described_class::QUERY_LIMIT))
+ .and_call_original
+
+ expect(described_class).to receive(:perform_async).with(nil, project.id)
+
+ run_worker
+
+ expect(project.issues.where(relative_position: nil)).to exist
+ end
+
+ it 'is eventually correct' do
+ prefix = project.issues.where.not(relative_position: nil).order(:relative_position).to_a
+ moved = project.issues.where.not(id: prefix.map(&:id))
+
+ run_worker
+
+ expect(project.issues.where(relative_position: nil)).to exist
+
+ run_worker
+
+ expect(project.issues.where(relative_position: nil)).not_to exist
+ expect(project.issues.order(:relative_position)).to eq(prefix + moved.order(:created_at, :id))
+ end
+ end
+
+ context 'we are passed bad IDs' do
+ let(:issue_id) { non_existing_record_id }
+ let(:project_id) { non_existing_record_id }
+
+ def max_positions_by_project
+ Issue
+ .group(:project_id)
+ .pluck(:project_id, Issue.arel_table[:relative_position].maximum.as('max_relative_position'))
+ .to_h
+ end
+
+ it 'does move any issues to the end' do
+ expect { run_worker }.not_to change { max_positions_by_project }
+ end
+
+ context 'the project_id refers to an empty project' do
+ let!(:project_id) { create(:project).id }
+
+ it 'does move any issues to the end' do
+ expect { run_worker }.not_to change { max_positions_by_project }
+ end
+ end
+ end
+
+ it 'anticipates the failure to place the issues, and schedules rebalancing' do
+ allow(Issue).to receive(:move_nulls_to_end) { raise RelativePositioning::NoSpaceLeft }
+
+ expect(Issues::RebalancingWorker).to receive(:perform_async).with(nil, nil, project.group.id)
+ expect(Gitlab::ErrorTracking)
+ .to receive(:log_exception)
+ .with(RelativePositioning::NoSpaceLeft, worker_arguments)
+
+ run_worker
+ end
+ end
+
+ context 'passing an issue ID' do
+ def run_worker
+ described_class.new.perform(issue_id)
+ end
+
+ let(:worker_arguments) { { issue_id: issue_id, project_id: nil } }
+
+ it_behaves_like 'running the issue placement worker'
+
+ context 'when block_issue_repositioning is enabled' do
+ let(:issue_id) { issue.id }
+ let(:project_id) { project.id }
+
+ before do
+ stub_feature_flags(block_issue_repositioning: group)
+ end
+
+ it 'does not run repositioning tasks' do
+ expect { run_worker }.not_to change { issue.reset.relative_position }
+ end
+ end
+ end
+
+ context 'passing a project ID' do
+ def run_worker
+ described_class.new.perform(nil, project_id)
+ end
+
+ let(:worker_arguments) { { issue_id: nil, project_id: project_id } }
+
+ it_behaves_like 'running the issue placement worker'
+ end
+ end
+
+ it 'has the `until_executed` deduplicate strategy' do
+ expect(described_class.get_deduplicate_strategy).to eq(:until_executed)
+ expect(described_class.get_deduplication_options).to include({ including_scheduled: true })
+ end
+end
diff --git a/spec/workers/issues/rebalancing_worker_spec.rb b/spec/workers/issues/rebalancing_worker_spec.rb
new file mode 100644
index 00000000000..438edd85f66
--- /dev/null
+++ b/spec/workers/issues/rebalancing_worker_spec.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Issues::RebalancingWorker do
+ describe '#perform' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:issue) { create(:issue, project: project) }
+
+ shared_examples 'running the worker' do
+ it 'runs an instance of Issues::RelativePositionRebalancingService' do
+ service = double(execute: nil)
+ service_param = arguments.second.present? ? kind_of(Project.id_in([project]).class) : kind_of(group&.all_projects.class)
+
+ expect(Issues::RelativePositionRebalancingService).to receive(:new).with(service_param).and_return(service)
+
+ described_class.new.perform(*arguments)
+ end
+
+ it 'anticipates there being too many concurent rebalances' do
+ service = double
+ service_param = arguments.second.present? ? kind_of(Project.id_in([project]).class) : kind_of(group&.all_projects.class)
+
+ allow(service).to receive(:execute).and_raise(Issues::RelativePositionRebalancingService::TooManyConcurrentRebalances)
+ expect(Issues::RelativePositionRebalancingService).to receive(:new).with(service_param).and_return(service)
+ expect(Gitlab::ErrorTracking).to receive(:log_exception).with(Issues::RelativePositionRebalancingService::TooManyConcurrentRebalances, include(project_id: arguments.second, root_namespace_id: arguments.third))
+
+ described_class.new.perform(*arguments)
+ end
+
+ it 'takes no action if the value is nil' do
+ expect(Issues::RelativePositionRebalancingService).not_to receive(:new)
+ expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
+
+ described_class.new.perform # all arguments are nil
+ end
+ end
+
+ shared_examples 'safely handles non-existent ids' do
+ it 'anticipates the inability to find the issue' do
+ expect(Gitlab::ErrorTracking).to receive(:log_exception).with(ArgumentError, include(project_id: arguments.second, root_namespace_id: arguments.third))
+ expect(Issues::RelativePositionRebalancingService).not_to receive(:new)
+
+ described_class.new.perform(*arguments)
+ end
+ end
+
+ context 'without root_namespace param' do
+ it_behaves_like 'running the worker' do
+ let(:arguments) { [-1, project.id] }
+ end
+
+ it_behaves_like 'safely handles non-existent ids' do
+ let(:arguments) { [nil, -1] }
+ end
+
+ include_examples 'an idempotent worker' do
+ let(:job_args) { [-1, project.id] }
+ end
+
+ include_examples 'an idempotent worker' do
+ let(:job_args) { [nil, -1] }
+ end
+ end
+
+ context 'with root_namespace param' do
+ it_behaves_like 'running the worker' do
+ let(:arguments) { [nil, nil, group.id] }
+ end
+
+ it_behaves_like 'safely handles non-existent ids' do
+ let(:arguments) { [nil, nil, -1] }
+ end
+
+ include_examples 'an idempotent worker' do
+ let(:job_args) { [nil, nil, group.id] }
+ end
+
+ include_examples 'an idempotent worker' do
+ let(:job_args) { [nil, nil, -1] }
+ end
+ end
+ end
+
+ it 'has the `until_executed` deduplicate strategy' do
+ expect(described_class.get_deduplicate_strategy).to eq(:until_executed)
+ expect(described_class.get_deduplication_options).to include({ including_scheduled: true })
+ end
+end
diff --git a/spec/workers/issues/reschedule_stuck_issue_rebalances_worker_spec.rb b/spec/workers/issues/reschedule_stuck_issue_rebalances_worker_spec.rb
new file mode 100644
index 00000000000..02d1241d2ba
--- /dev/null
+++ b/spec/workers/issues/reschedule_stuck_issue_rebalances_worker_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Issues::RescheduleStuckIssueRebalancesWorker, :clean_gitlab_redis_shared_state do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+
+ subject(:worker) { described_class.new }
+
+ describe '#perform' do
+ it 'does not schedule a rebalance' do
+ expect(IssueRebalancingWorker).not_to receive(:perform_async)
+
+ worker.perform
+ end
+
+ it 'schedules a rebalance in case there are any rebalances started' do
+ expect(::Gitlab::Issues::Rebalancing::State).to receive(:fetch_rebalancing_groups_and_projects).and_return([[group.id], [project.id]])
+ expect(IssueRebalancingWorker).to receive(:bulk_perform_async).with([[nil, nil, group.id]]).once
+ expect(IssueRebalancingWorker).to receive(:bulk_perform_async).with([[nil, project.id, nil]]).once
+
+ worker.perform
+ end
+ end
+end
diff --git a/spec/workers/loose_foreign_keys/cleanup_worker_spec.rb b/spec/workers/loose_foreign_keys/cleanup_worker_spec.rb
new file mode 100644
index 00000000000..544be2a69a6
--- /dev/null
+++ b/spec/workers/loose_foreign_keys/cleanup_worker_spec.rb
@@ -0,0 +1,153 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe LooseForeignKeys::CleanupWorker do
+ include MigrationsHelpers
+
+ def create_table_structure
+ migration = ActiveRecord::Migration.new.extend(Gitlab::Database::MigrationHelpers::LooseForeignKeyHelpers)
+
+ migration.create_table :_test_loose_fk_parent_table_1
+ migration.create_table :_test_loose_fk_parent_table_2
+
+ migration.create_table :_test_loose_fk_child_table_1_1 do |t|
+ t.bigint :parent_id
+ end
+
+ migration.create_table :_test_loose_fk_child_table_1_2 do |t|
+ t.bigint :parent_id_with_different_column
+ end
+
+ migration.create_table :_test_loose_fk_child_table_2_1 do |t|
+ t.bigint :parent_id
+ end
+
+ migration.track_record_deletions(:_test_loose_fk_parent_table_1)
+ migration.track_record_deletions(:_test_loose_fk_parent_table_2)
+ end
+
+ let!(:parent_model_1) do
+ Class.new(ApplicationRecord) do
+ self.table_name = '_test_loose_fk_parent_table_1'
+
+ include LooseForeignKey
+
+ loose_foreign_key :_test_loose_fk_child_table_1_1, :parent_id, on_delete: :async_delete
+ loose_foreign_key :_test_loose_fk_child_table_1_2, :parent_id_with_different_column, on_delete: :async_nullify
+ end
+ end
+
+ let!(:parent_model_2) do
+ Class.new(ApplicationRecord) do
+ self.table_name = '_test_loose_fk_parent_table_2'
+
+ include LooseForeignKey
+
+ loose_foreign_key :_test_loose_fk_child_table_2_1, :parent_id, on_delete: :async_delete
+ end
+ end
+
+ let!(:child_model_1) do
+ Class.new(ApplicationRecord) do
+ self.table_name = '_test_loose_fk_child_table_1_1'
+ end
+ end
+
+ let!(:child_model_2) do
+ Class.new(ApplicationRecord) do
+ self.table_name = '_test_loose_fk_child_table_1_2'
+ end
+ end
+
+ let!(:child_model_3) do
+ Class.new(ApplicationRecord) do
+ self.table_name = '_test_loose_fk_child_table_2_1'
+ end
+ end
+
+ let(:loose_fk_parent_table_1) { table(:_test_loose_fk_parent_table_1) }
+ let(:loose_fk_parent_table_2) { table(:_test_loose_fk_parent_table_2) }
+ let(:loose_fk_child_table_1_1) { table(:_test_loose_fk_child_table_1_1) }
+ let(:loose_fk_child_table_1_2) { table(:_test_loose_fk_child_table_1_2) }
+ let(:loose_fk_child_table_2_1) { table(:_test_loose_fk_child_table_2_1) }
+
+ before(:all) do
+ create_table_structure
+ end
+
+ after(:all) do
+ migration = ActiveRecord::Migration.new
+
+ migration.drop_table :_test_loose_fk_parent_table_1
+ migration.drop_table :_test_loose_fk_parent_table_2
+ migration.drop_table :_test_loose_fk_child_table_1_1
+ migration.drop_table :_test_loose_fk_child_table_1_2
+ migration.drop_table :_test_loose_fk_child_table_2_1
+ end
+
+ before do
+ parent_record_1 = loose_fk_parent_table_1.create!
+ loose_fk_child_table_1_1.create!(parent_id: parent_record_1.id)
+ loose_fk_child_table_1_2.create!(parent_id_with_different_column: parent_record_1.id)
+
+ parent_record_2 = loose_fk_parent_table_1.create!
+ 2.times { loose_fk_child_table_1_1.create!(parent_id: parent_record_2.id) }
+ 3.times { loose_fk_child_table_1_2.create!(parent_id_with_different_column: parent_record_2.id) }
+
+ parent_record_3 = loose_fk_parent_table_2.create!
+ 5.times { loose_fk_child_table_2_1.create!(parent_id: parent_record_3.id) }
+
+ parent_model_1.delete_all
+ parent_model_2.delete_all
+ end
+
+ it 'cleans up all rows' do
+ described_class.new.perform
+
+ expect(loose_fk_child_table_1_1.count).to eq(0)
+ expect(loose_fk_child_table_1_2.where(parent_id_with_different_column: nil).count).to eq(4)
+ expect(loose_fk_child_table_2_1.count).to eq(0)
+ end
+
+ context 'when deleting in batches' do
+ before do
+ stub_const('LooseForeignKeys::CleanupWorker::BATCH_SIZE', 2)
+ end
+
+ it 'cleans up all rows' do
+ expect(LooseForeignKeys::BatchCleanerService).to receive(:new).exactly(:twice).and_call_original
+
+ described_class.new.perform
+
+ expect(loose_fk_child_table_1_1.count).to eq(0)
+ expect(loose_fk_child_table_1_2.where(parent_id_with_different_column: nil).count).to eq(4)
+ expect(loose_fk_child_table_2_1.count).to eq(0)
+ end
+ end
+
+ context 'when the deleted rows count limit have been reached' do
+ def count_deletable_rows
+ loose_fk_child_table_1_1.count + loose_fk_child_table_2_1.count
+ end
+
+ before do
+ stub_const('LooseForeignKeys::ModificationTracker::MAX_DELETES', 2)
+ stub_const('LooseForeignKeys::CleanerService::DELETE_LIMIT', 1)
+ end
+
+ it 'cleans up 2 rows' do
+ expect { described_class.new.perform }.to change { count_deletable_rows }.by(-2)
+ end
+ end
+
+ context 'when the loose_foreign_key_cleanup feature flag is off' do
+ before do
+ stub_feature_flags(loose_foreign_key_cleanup: false)
+ end
+
+ it 'does nothing' do
+ expect { described_class.new.perform }.not_to change { LooseForeignKeys::DeletedRecord.status_processed.count }
+ end
+ end
+end
diff --git a/spec/workers/namespaces/invite_team_email_worker_spec.rb b/spec/workers/namespaces/invite_team_email_worker_spec.rb
new file mode 100644
index 00000000000..47fdff9a8ef
--- /dev/null
+++ b/spec/workers/namespaces/invite_team_email_worker_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Namespaces::InviteTeamEmailWorker do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+
+ it 'sends the email' do
+ expect(Namespaces::InviteTeamEmailService).to receive(:send_email).with(user, group).once
+ subject.perform(group.id, user.id)
+ end
+
+ context 'when user id is non-existent' do
+ it 'does not send the email' do
+ expect(Namespaces::InviteTeamEmailService).not_to receive(:send_email)
+ subject.perform(group.id, non_existing_record_id)
+ end
+ end
+
+ context 'when group id is non-existent' do
+ it 'does not send the email' do
+ expect(Namespaces::InviteTeamEmailService).not_to receive(:send_email)
+ subject.perform(non_existing_record_id, user.id)
+ end
+ end
+end
diff --git a/spec/workers/packages/maven/metadata/sync_worker_spec.rb b/spec/workers/packages/maven/metadata/sync_worker_spec.rb
index 10482b3e327..4b3cc6f964b 100644
--- a/spec/workers/packages/maven/metadata/sync_worker_spec.rb
+++ b/spec/workers/packages/maven/metadata/sync_worker_spec.rb
@@ -8,6 +8,7 @@ RSpec.describe Packages::Maven::Metadata::SyncWorker, type: :worker do
let(:versions) { %w[1.2 1.1 2.1 3.0-SNAPSHOT] }
let(:worker) { described_class.new }
+ let(:data_struct) { Struct.new(:release, :latest, :versions, keyword_init: true) }
describe '#perform' do
let(:user) { create(:user) }
@@ -197,7 +198,7 @@ RSpec.describe Packages::Maven::Metadata::SyncWorker, type: :worker do
def versions_from(xml_content)
xml_doc = Nokogiri::XML(xml_content)
- OpenStruct.new(
+ data_struct.new(
release: xml_doc.xpath('//metadata/versioning/release').first.content,
latest: xml_doc.xpath('//metadata/versioning/latest').first.content,
versions: xml_doc.xpath('//metadata/versioning/versions/version').map(&:content)
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 039f86f1911..42e39c51a88 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -91,14 +91,6 @@ RSpec.describe PostReceive do
perform
end
-
- it 'tracks an event for the empty_repo_upload experiment', :experiment do
- expect_next_instance_of(EmptyRepoUploadExperiment) do |e|
- expect(e).to receive(:track_initial_write)
- end
-
- perform
- end
end
shared_examples 'not updating remote mirrors' do
diff --git a/spec/workers/propagate_integration_group_worker_spec.rb b/spec/workers/propagate_integration_group_worker_spec.rb
index 9d46534df4f..60442438a1d 100644
--- a/spec/workers/propagate_integration_group_worker_spec.rb
+++ b/spec/workers/propagate_integration_group_worker_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe PropagateIntegrationGroupWorker do
end
context 'with a group integration' do
- let_it_be(:integration) { create(:redmine_integration, group: group, project: nil) }
+ let_it_be(:integration) { create(:redmine_integration, :group, group: group) }
it 'calls to BulkCreateIntegrationService' do
expect(BulkCreateIntegrationService).to receive(:new)
diff --git a/spec/workers/propagate_integration_inherit_descendant_worker_spec.rb b/spec/workers/propagate_integration_inherit_descendant_worker_spec.rb
index 8a231d4104c..c9a7bfaa8b6 100644
--- a/spec/workers/propagate_integration_inherit_descendant_worker_spec.rb
+++ b/spec/workers/propagate_integration_inherit_descendant_worker_spec.rb
@@ -5,8 +5,8 @@ require 'spec_helper'
RSpec.describe PropagateIntegrationInheritDescendantWorker do
let_it_be(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group) }
- let_it_be(:group_integration) { create(:redmine_integration, group: group, project: nil) }
- let_it_be(:subgroup_integration) { create(:redmine_integration, group: subgroup, project: nil, inherit_from_id: group_integration.id) }
+ let_it_be(:group_integration) { create(:redmine_integration, :group, group: group) }
+ let_it_be(:subgroup_integration) { create(:redmine_integration, :group, group: subgroup, inherit_from_id: group_integration.id) }
it_behaves_like 'an idempotent worker' do
let(:job_args) { [group_integration.id, subgroup_integration.id, subgroup_integration.id] }
diff --git a/spec/workers/propagate_integration_project_worker_spec.rb b/spec/workers/propagate_integration_project_worker_spec.rb
index 312631252cc..c7adf1b826f 100644
--- a/spec/workers/propagate_integration_project_worker_spec.rb
+++ b/spec/workers/propagate_integration_project_worker_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe PropagateIntegrationProjectWorker do
end
context 'with a group integration' do
- let_it_be(:integration) { create(:redmine_integration, group: group, project: nil) }
+ let_it_be(:integration) { create(:redmine_integration, :group, group: group) }
it 'calls to BulkCreateIntegrationService' do
expect(BulkCreateIntegrationService).to receive(:new)
diff --git a/spec/workers/ssh_keys/expired_notification_worker_spec.rb b/spec/workers/ssh_keys/expired_notification_worker_spec.rb
index 109d24f03ab..be38391ff8c 100644
--- a/spec/workers/ssh_keys/expired_notification_worker_spec.rb
+++ b/spec/workers/ssh_keys/expired_notification_worker_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe SshKeys::ExpiredNotificationWorker, type: :worker do
stub_const("SshKeys::ExpiredNotificationWorker::BATCH_SIZE", 5)
end
- let_it_be_with_reload(:keys) { create_list(:key, 20, expires_at: 3.days.ago, user: user) }
+ let_it_be_with_reload(:keys) { create_list(:key, 20, expires_at: Time.current, user: user) }
it 'updates all keys regardless of batch size' do
worker.perform
@@ -54,8 +54,8 @@ RSpec.describe SshKeys::ExpiredNotificationWorker, type: :worker do
context 'when key has expired in the past' do
let_it_be(:expired_past) { create(:key, expires_at: 1.day.ago, user: user) }
- it 'does update notified column' do
- expect { worker.perform }.to change { expired_past.reload.expiry_notification_delivered_at }
+ it 'does not update notified column' do
+ expect { worker.perform }.not_to change { expired_past.reload.expiry_notification_delivered_at }
end
context 'when key has already been notified of expiration' do
diff --git a/spec/workers/tasks_to_be_done/create_worker_spec.rb b/spec/workers/tasks_to_be_done/create_worker_spec.rb
new file mode 100644
index 00000000000..a158872273f
--- /dev/null
+++ b/spec/workers/tasks_to_be_done/create_worker_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe TasksToBeDone::CreateWorker do
+ let_it_be(:member_task) { create(:member_task, tasks: MemberTask::TASKS.values) }
+ let_it_be(:current_user) { create(:user) }
+
+ let(:assignee_ids) { [1, 2] }
+ let(:job_args) { [member_task.id, current_user.id, assignee_ids] }
+
+ before do
+ member_task.project.group.add_owner(current_user)
+ end
+
+ describe '.perform' do
+ it 'executes the task services for all tasks to be done', :aggregate_failures do
+ MemberTask::TASKS.each_key do |task|
+ service_class = "TasksToBeDone::Create#{task.to_s.camelize}TaskService".constantize
+
+ expect(service_class)
+ .to receive(:new)
+ .with(project: member_task.project, current_user: current_user, assignee_ids: assignee_ids)
+ .and_call_original
+ end
+
+ expect { described_class.new.perform(*job_args) }.to change(Issue, :count).by(3)
+ end
+ end
+
+ include_examples 'an idempotent worker' do
+ it 'creates 3 task issues' do
+ expect { subject }.to change(Issue, :count).by(3)
+ end
+ end
+end
diff --git a/spec/workers/users/deactivate_dormant_users_worker_spec.rb b/spec/workers/users/deactivate_dormant_users_worker_spec.rb
index 934c497c79a..20cd55e19eb 100644
--- a/spec/workers/users/deactivate_dormant_users_worker_spec.rb
+++ b/spec/workers/users/deactivate_dormant_users_worker_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Users::DeactivateDormantUsersWorker do
+ using RSpec::Parameterized::TableSyntax
+
describe '#perform' do
let_it_be(:dormant) { create(:user, last_activity_on: User::MINIMUM_INACTIVE_DAYS.days.ago.to_date) }
let_it_be(:inactive) { create(:user, last_activity_on: nil) }
@@ -22,12 +24,12 @@ RSpec.describe Users::DeactivateDormantUsersWorker do
context 'when automatic deactivation of dormant users is enabled' do
before do
stub_application_setting(deactivate_dormant_users: true)
+ stub_const("#{described_class.name}::PAUSE_SECONDS", 0)
end
it 'deactivates dormant users' do
freeze_time do
stub_const("#{described_class.name}::BATCH_SIZE", 1)
- stub_const("#{described_class.name}::PAUSE_SECONDS", 0)
expect(worker).to receive(:sleep).twice
@@ -37,6 +39,38 @@ RSpec.describe Users::DeactivateDormantUsersWorker do
expect(User.with_no_activity.count).to eq(0)
end
end
+
+ where(:user_type, :expected_state) do
+ :human | 'deactivated'
+ :support_bot | 'active'
+ :alert_bot | 'active'
+ :visual_review_bot | 'active'
+ :service_user | 'deactivated'
+ :ghost | 'active'
+ :project_bot | 'active'
+ :migration_bot | 'active'
+ :security_bot | 'active'
+ :automation_bot | 'active'
+ end
+ with_them do
+ it 'deactivates certain user types' do
+ user = create(:user, user_type: user_type, state: :active, last_activity_on: User::MINIMUM_INACTIVE_DAYS.days.ago.to_date)
+
+ worker.perform
+
+ expect(user.reload.state).to eq(expected_state)
+ end
+ end
+
+ it 'does not deactivate non-active users' do
+ human_user = create(:user, user_type: :human, state: :blocked, last_activity_on: User::MINIMUM_INACTIVE_DAYS.days.ago.to_date)
+ service_user = create(:user, user_type: :service_user, state: :blocked, last_activity_on: User::MINIMUM_INACTIVE_DAYS.days.ago.to_date)
+
+ worker.perform
+
+ expect(human_user.reload.state).to eq('blocked')
+ expect(service_user.reload.state).to eq('blocked')
+ end
end
context 'when automatic deactivation of dormant users is disabled' do